@tiwater/office-mcp 0.7.0 → 0.8.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 +4 -4
- package/office/index.mjs +134 -15
- package/package.json +1 -1
package/office/README.md
CHANGED
|
@@ -10,8 +10,8 @@ Shared stdio MCP server for Office document workflows.
|
|
|
10
10
|
- `docx_migrate_template`
|
|
11
11
|
- `docx_verify_migration`
|
|
12
12
|
- `docx_compare`
|
|
13
|
-
- `docx_validate_template_transform`
|
|
14
13
|
- `docx_export_json`
|
|
14
|
+
- `office_render_pdf`
|
|
15
15
|
- `xlsx_inspect`
|
|
16
16
|
- `xlsx_export_json`
|
|
17
17
|
- `xlsx_validate`
|
|
@@ -23,9 +23,9 @@ Shared stdio MCP server for Office document workflows.
|
|
|
23
23
|
Install `@tiwater/office-mcp` together with the runtime versions required by
|
|
24
24
|
the consumer, then run `tiwater-office-mcp` as a stdio MCP server.
|
|
25
25
|
|
|
26
|
-
The server invokes published `tiwater-docx`, `tiwater-xlsx`,
|
|
27
|
-
`tiwater-pptx` commands from `PATH`. It does not require
|
|
28
|
-
fall back to local projects.
|
|
26
|
+
The server invokes published `tiwater-docx`, `tiwater-xlsx`,
|
|
27
|
+
`tiwater-pptx`, and `tiwater-convert` commands from `PATH`. It does not require
|
|
28
|
+
a source checkout or fall back to local projects.
|
|
29
29
|
|
|
30
30
|
The official MCP SDK derives the schemas advertised to clients and validates
|
|
31
31
|
tool arguments and structured results before they cross the protocol boundary.
|
package/office/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
6
7
|
import { isDeepStrictEqual } from 'node:util';
|
|
@@ -30,6 +31,10 @@ const pptxCandidates = [
|
|
|
30
31
|
commandCandidate('tiwater-pptx', [], { cwd: invocationCwd }),
|
|
31
32
|
];
|
|
32
33
|
|
|
34
|
+
const convertCandidates = [
|
|
35
|
+
commandCandidate('tiwater-convert', [], { cwd: invocationCwd }),
|
|
36
|
+
];
|
|
37
|
+
|
|
33
38
|
const pathInput = z.string().trim().min(1);
|
|
34
39
|
const migrationAction = z.enum([
|
|
35
40
|
'place-content',
|
|
@@ -136,6 +141,42 @@ const artifact = z.object({
|
|
|
136
141
|
bytes: z.number().int().nonnegative(),
|
|
137
142
|
}).strict();
|
|
138
143
|
|
|
144
|
+
const renderFileIdentity = z.object({
|
|
145
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
146
|
+
size_bytes: z.number().int().positive(),
|
|
147
|
+
}).strict();
|
|
148
|
+
const nativeRenderReceipt = z.object({
|
|
149
|
+
status: z.literal('ok'),
|
|
150
|
+
input: z.string(),
|
|
151
|
+
input_sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
152
|
+
output: z.string(),
|
|
153
|
+
output_sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
154
|
+
source_format: z.enum(['doc', 'docx', 'odt', 'rtf', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp']),
|
|
155
|
+
target_format: z.literal('pdf'),
|
|
156
|
+
version: z.string(),
|
|
157
|
+
backend: z.enum(['wps', 'et', 'wpp']),
|
|
158
|
+
fallback_reason: z.null(),
|
|
159
|
+
page_count: z.number().int().positive(),
|
|
160
|
+
native_render_provenance: z.object({
|
|
161
|
+
schema: z.literal('tiwater.convert-native-render-provenance/v1'),
|
|
162
|
+
backend: z.enum(['wps', 'et', 'wpp']),
|
|
163
|
+
input: renderFileIdentity,
|
|
164
|
+
output: renderFileIdentity,
|
|
165
|
+
page_count: z.number().int().positive(),
|
|
166
|
+
}).passthrough(),
|
|
167
|
+
}).passthrough();
|
|
168
|
+
const nativeRenderOutput = z.object({
|
|
169
|
+
tool: z.literal('office_render_pdf'),
|
|
170
|
+
runtime: runtimeIdentity,
|
|
171
|
+
pdf: artifact,
|
|
172
|
+
receipt: artifact,
|
|
173
|
+
summary: z.object({
|
|
174
|
+
sourceFormat: z.string(),
|
|
175
|
+
backend: z.enum(['wps', 'et', 'wpp']),
|
|
176
|
+
pageCount: z.number().int().positive(),
|
|
177
|
+
}).strict(),
|
|
178
|
+
}).strict();
|
|
179
|
+
|
|
139
180
|
const migrationCatalogOutput = z.object({
|
|
140
181
|
tool: z.literal('docx_list_migration_choices'),
|
|
141
182
|
runtime: runtimeIdentity,
|
|
@@ -291,13 +332,6 @@ const tools = [
|
|
|
291
332
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
292
333
|
handler: docxCompare,
|
|
293
334
|
},
|
|
294
|
-
{
|
|
295
|
-
name: 'docx_validate_template_transform',
|
|
296
|
-
description: 'Validate whether a source DOCX template and target DOCX template are structurally compatible.',
|
|
297
|
-
inputSchema: z.object({ sourceTemplate: pathInput, targetTemplate: pathInput }).strict(),
|
|
298
|
-
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
299
|
-
handler: docxValidateTemplateTransform,
|
|
300
|
-
},
|
|
301
335
|
{
|
|
302
336
|
name: 'docx_export_json',
|
|
303
337
|
description: 'Export DOCX body content to a new JSON artifact without returning the full document through MCP.',
|
|
@@ -305,6 +339,17 @@ const tools = [
|
|
|
305
339
|
outputSchema: artifactOutput('docx_export_json'),
|
|
306
340
|
handler: docxExportJson,
|
|
307
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: 'office_render_pdf',
|
|
344
|
+
description: 'Render a current Office document to PDF with its required native WPS backend and write the complete provider receipt as evidence. The input extension selects Writer, Spreadsheets, or Presentation; fallback rendering is rejected.',
|
|
345
|
+
inputSchema: z.object({
|
|
346
|
+
input: pathInput.describe('Path to the current Office document.'),
|
|
347
|
+
output: pathInput.describe('New PDF output path. Existing files are never overwritten.'),
|
|
348
|
+
receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
|
|
349
|
+
}).strict(),
|
|
350
|
+
outputSchema: nativeRenderOutput,
|
|
351
|
+
handler: officeRenderPdf,
|
|
352
|
+
},
|
|
308
353
|
{
|
|
309
354
|
name: 'xlsx_inspect',
|
|
310
355
|
description: 'Inspect an XLSX workbook and write one JSON observation containing workbook structure, exported values, formulas, styles, merged ranges, and conversion evidence.',
|
|
@@ -658,13 +703,6 @@ async function docxCompare(args) {
|
|
|
658
703
|
return { tool: 'docx_compare', runtime: commandRuntime(result), report: result.json };
|
|
659
704
|
}
|
|
660
705
|
|
|
661
|
-
async function docxValidateTemplateTransform(args) {
|
|
662
|
-
const sourceTemplate = requireString(args.sourceTemplate, 'sourceTemplate');
|
|
663
|
-
const targetTemplate = requireString(args.targetTemplate, 'targetTemplate');
|
|
664
|
-
const result = await runJsonCandidateChain(docxCandidates, ['validate-template-transform', sourceTemplate, targetTemplate, '--json']);
|
|
665
|
-
return { tool: 'docx_validate_template_transform', runtime: commandRuntime(result), report: result.json };
|
|
666
|
-
}
|
|
667
|
-
|
|
668
706
|
async function docxExportJson(args) {
|
|
669
707
|
const input = requireString(args.input, 'input');
|
|
670
708
|
const result = await runJsonCandidateChain(docxCandidates, ['export-json', input]);
|
|
@@ -675,6 +713,64 @@ async function docxExportJson(args) {
|
|
|
675
713
|
};
|
|
676
714
|
}
|
|
677
715
|
|
|
716
|
+
async function officeRenderPdf(args) {
|
|
717
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
718
|
+
const output = path.resolve(requireString(args.output, 'output'));
|
|
719
|
+
const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
|
|
720
|
+
const sourceFormat = path.extname(input).slice(1).toLowerCase();
|
|
721
|
+
const backend = nativeRenderBackend(sourceFormat);
|
|
722
|
+
if (path.extname(output).toLowerCase() !== '.pdf') {
|
|
723
|
+
throw Object.assign(new Error(`Office render output must be a PDF: ${output}`), { code: -32602 });
|
|
724
|
+
}
|
|
725
|
+
const inputArtifact = await fileArtifact(input);
|
|
726
|
+
await requireNewFile(output, 'output');
|
|
727
|
+
await requireNewFile(receiptOutput, 'receiptOutput');
|
|
728
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
729
|
+
try {
|
|
730
|
+
const result = await runJsonCandidateChain(
|
|
731
|
+
convertCandidates,
|
|
732
|
+
[`${sourceFormat}-to-pdf`, input, output],
|
|
733
|
+
{ env: { TIWATER_OFFICE_PDF_BACKEND: backend } });
|
|
734
|
+
const receipt = nativeRenderReceipt.parse(result.json);
|
|
735
|
+
const pdf = await fileArtifact(output);
|
|
736
|
+
if (path.resolve(receipt.input) !== input
|
|
737
|
+
|| path.resolve(receipt.output) !== output
|
|
738
|
+
|| receipt.source_format !== sourceFormat
|
|
739
|
+
|| receipt.backend !== backend
|
|
740
|
+
|| receipt.native_render_provenance.backend !== backend
|
|
741
|
+
|| receipt.page_count !== receipt.native_render_provenance.page_count
|
|
742
|
+
|| receipt.input_sha256 !== inputArtifact.sha256
|
|
743
|
+
|| receipt.native_render_provenance.input.sha256 !== inputArtifact.sha256
|
|
744
|
+
|| receipt.native_render_provenance.input.size_bytes !== inputArtifact.bytes
|
|
745
|
+
|| receipt.output_sha256 !== pdf.sha256
|
|
746
|
+
|| receipt.native_render_provenance.output.sha256 !== pdf.sha256
|
|
747
|
+
|| receipt.native_render_provenance.output.size_bytes !== pdf.bytes) {
|
|
748
|
+
throw new Error('Native Office render receipt is not bound to the current input and output');
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
tool: 'office_render_pdf',
|
|
752
|
+
runtime: commandRuntime(result),
|
|
753
|
+
pdf,
|
|
754
|
+
receipt: await writeJsonArtifact(receiptOutput, receipt),
|
|
755
|
+
summary: {
|
|
756
|
+
sourceFormat,
|
|
757
|
+
backend,
|
|
758
|
+
pageCount: receipt.page_count,
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
} catch (error) {
|
|
762
|
+
await rm(output, { force: true });
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function nativeRenderBackend(sourceFormat) {
|
|
768
|
+
if (['doc', 'docx', 'odt', 'rtf'].includes(sourceFormat)) return 'wps';
|
|
769
|
+
if (['xls', 'xlsx', 'ods'].includes(sourceFormat)) return 'et';
|
|
770
|
+
if (['ppt', 'pptx', 'odp'].includes(sourceFormat)) return 'wpp';
|
|
771
|
+
throw Object.assign(new Error(`Unsupported Office render input: .${sourceFormat || '(none)'}`), { code: -32602 });
|
|
772
|
+
}
|
|
773
|
+
|
|
678
774
|
async function xlsxInspect(args) {
|
|
679
775
|
const input = requireString(args.input, 'input');
|
|
680
776
|
const result = await runJsonCandidateChain(xlsxCandidates, ['inspect', input, '--json']);
|
|
@@ -737,6 +833,29 @@ async function writeJsonArtifact(output, payload) {
|
|
|
737
833
|
};
|
|
738
834
|
}
|
|
739
835
|
|
|
836
|
+
async function fileArtifact(filePath) {
|
|
837
|
+
const hash = createHash('sha256');
|
|
838
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
839
|
+
hash.update(chunk);
|
|
840
|
+
}
|
|
841
|
+
const file = await stat(filePath);
|
|
842
|
+
return {
|
|
843
|
+
path: path.resolve(filePath),
|
|
844
|
+
sha256: hash.digest('hex'),
|
|
845
|
+
bytes: file.size,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function requireNewFile(filePath, label) {
|
|
850
|
+
try {
|
|
851
|
+
await stat(filePath);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (error?.code === 'ENOENT') return;
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
throw Object.assign(new Error(`${label} already exists: ${filePath}`), { code: -32602 });
|
|
857
|
+
}
|
|
858
|
+
|
|
740
859
|
function commandRuntime(result) {
|
|
741
860
|
return {
|
|
742
861
|
command: result.command,
|