@tiwater/office-mcp 0.2.0 → 0.3.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/_shared/tool-runtime.mjs +1 -1
- package/office/README.md +5 -1
- package/office/index.mjs +232 -293
- package/package.json +9 -2
- package/_shared/mcp-stdio.mjs +0 -152
package/_shared/tool-runtime.mjs
CHANGED
|
@@ -99,7 +99,7 @@ async function runCommand(candidate, args, options) {
|
|
|
99
99
|
child.on('close', code => {
|
|
100
100
|
const allowedExitCodes = options.allowedExitCodes ?? [0];
|
|
101
101
|
if (allowedExitCodes.includes(code)) {
|
|
102
|
-
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs });
|
|
102
|
+
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs, cwd });
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
|
package/office/README.md
CHANGED
|
@@ -15,7 +15,6 @@ Shared stdio MCP server for Office document workflows.
|
|
|
15
15
|
- `xlsx_export_json`
|
|
16
16
|
- `xlsx_validate`
|
|
17
17
|
- `pptx_inspect`
|
|
18
|
-
- `pptx_inspect_detail`
|
|
19
18
|
- `pptx_export_json`
|
|
20
19
|
|
|
21
20
|
## Run
|
|
@@ -26,3 +25,8 @@ the consumer, then run `tiwater-office-mcp` as a stdio MCP server.
|
|
|
26
25
|
The server invokes published `tiwater-docx`, `tiwater-xlsx`, and
|
|
27
26
|
`tiwater-pptx` commands from `PATH`. It does not require a source checkout or
|
|
28
27
|
fall back to local projects.
|
|
28
|
+
|
|
29
|
+
The official MCP SDK derives the schemas advertised to clients and validates
|
|
30
|
+
tool arguments and structured results before they cross the protocol boundary.
|
|
31
|
+
Large observations and exports are written to a caller-selected new JSON
|
|
32
|
+
artifact. MCP returns only the artifact path, hash, and byte count.
|
package/office/index.mjs
CHANGED
|
@@ -1,340 +1,269 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import {
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
7
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
8
|
+
import * as z from 'zod/v4';
|
|
5
9
|
import {
|
|
6
10
|
commandCandidate,
|
|
7
11
|
createToolResult,
|
|
8
|
-
maybeReadJson,
|
|
9
12
|
requireString,
|
|
10
|
-
resolveRepoPath,
|
|
11
|
-
runCandidateChain,
|
|
12
13
|
runJsonCandidateChain,
|
|
13
14
|
withTempJsonFile,
|
|
14
15
|
} from '../_shared/tool-runtime.mjs';
|
|
15
16
|
|
|
17
|
+
const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
18
|
+
const invocationCwd = process.cwd();
|
|
19
|
+
|
|
16
20
|
const docxCandidates = [
|
|
17
|
-
commandCandidate('tiwater-docx'),
|
|
21
|
+
commandCandidate('tiwater-docx', [], { cwd: invocationCwd }),
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
const xlsxCandidates = [
|
|
21
|
-
commandCandidate('tiwater-xlsx'),
|
|
25
|
+
commandCandidate('tiwater-xlsx', [], { cwd: invocationCwd }),
|
|
22
26
|
];
|
|
23
27
|
|
|
24
28
|
const pptxCandidates = [
|
|
25
|
-
commandCandidate('tiwater-pptx'),
|
|
29
|
+
commandCandidate('tiwater-pptx', [], { cwd: invocationCwd }),
|
|
26
30
|
];
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
32
|
+
const pathInput = z.string().trim().min(1);
|
|
33
|
+
const migrationAction = z.enum([
|
|
34
|
+
'place-content',
|
|
35
|
+
'keep-template-content',
|
|
36
|
+
'keep-template-label',
|
|
37
|
+
'select-template-option',
|
|
38
|
+
'exclude-source',
|
|
39
|
+
'review-source',
|
|
40
|
+
]);
|
|
41
|
+
const targetActions = new Set([
|
|
42
|
+
'place-content',
|
|
43
|
+
'keep-template-content',
|
|
44
|
+
'keep-template-label',
|
|
45
|
+
'select-template-option',
|
|
46
|
+
]);
|
|
47
|
+
const terminalActions = new Set(['exclude-source', 'review-source']);
|
|
48
|
+
|
|
49
|
+
const migrationChoiceInput = z.object({
|
|
50
|
+
sourceChoiceId: z.string().trim().min(1),
|
|
51
|
+
action: migrationAction,
|
|
52
|
+
targetChoiceId: z.string().trim().min(1).optional(),
|
|
53
|
+
cardinality: z.enum(['one', 'all']).optional(),
|
|
54
|
+
}).strict().superRefine((choice, context) => {
|
|
55
|
+
if (targetActions.has(choice.action) && !choice.targetChoiceId) {
|
|
56
|
+
context.addIssue({ code: 'custom', path: ['targetChoiceId'], message: `${choice.action} requires targetChoiceId` });
|
|
57
|
+
}
|
|
58
|
+
if (terminalActions.has(choice.action) && choice.targetChoiceId) {
|
|
59
|
+
context.addIssue({ code: 'custom', path: ['targetChoiceId'], message: `${choice.action} forbids targetChoiceId` });
|
|
60
|
+
}
|
|
61
|
+
if (choice.cardinality === 'all' && !terminalActions.has(choice.action)) {
|
|
62
|
+
context.addIssue({ code: 'custom', path: ['cardinality'], message: 'cardinality all is limited to terminal actions' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const templateCleanupInput = z.object({
|
|
67
|
+
targetChoiceId: z.string().trim().min(1),
|
|
68
|
+
scope: z.enum(['cell', 'row']),
|
|
69
|
+
}).strict();
|
|
70
|
+
|
|
71
|
+
const templateMigrationInput = z.object({
|
|
72
|
+
source: pathInput.describe('Path to the current source DOCX.'),
|
|
73
|
+
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
74
|
+
output: pathInput.describe('Path to the migrated output DOCX.'),
|
|
75
|
+
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source id returned by docx_list_migration_choices.'),
|
|
76
|
+
templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
|
|
77
|
+
}).strict();
|
|
78
|
+
|
|
79
|
+
const runtimeIdentity = z.object({
|
|
80
|
+
command: z.string(),
|
|
81
|
+
cwd: z.string(),
|
|
82
|
+
}).strict();
|
|
83
|
+
|
|
84
|
+
const migrationChoiceOutput = z.object({
|
|
85
|
+
id: z.string(),
|
|
86
|
+
kind: z.string(),
|
|
87
|
+
scope: z.string(),
|
|
88
|
+
text: z.string().nullable(),
|
|
89
|
+
count: z.number().int(),
|
|
90
|
+
requiredCardinality: z.string().nullable(),
|
|
91
|
+
context: z.record(z.string(), z.unknown()).nullable(),
|
|
92
|
+
allowedActions: z.array(z.string()),
|
|
93
|
+
}).strict();
|
|
94
|
+
|
|
95
|
+
const migrationCatalogOutput = z.object({
|
|
96
|
+
tool: z.literal('docx_list_migration_choices'),
|
|
97
|
+
runtime: runtimeIdentity,
|
|
98
|
+
catalog: z.object({
|
|
99
|
+
schema: z.string(),
|
|
100
|
+
pass: z.boolean(),
|
|
101
|
+
sourceSha256: z.string(),
|
|
102
|
+
baselineSha256: z.string(),
|
|
103
|
+
sources: z.array(migrationChoiceOutput),
|
|
104
|
+
targets: z.array(migrationChoiceOutput),
|
|
105
|
+
}).strict(),
|
|
106
|
+
}).strict();
|
|
107
|
+
|
|
108
|
+
function migrationReceiptOutput(tool) {
|
|
109
|
+
return z.object({
|
|
110
|
+
tool: z.literal(tool),
|
|
111
|
+
runtime: runtimeIdentity,
|
|
112
|
+
receipt: z.object({
|
|
113
|
+
schema: z.string(),
|
|
114
|
+
toolVersion: z.string(),
|
|
115
|
+
status: z.enum(['pass', 'review-required', 'failed']),
|
|
116
|
+
pass: z.boolean(),
|
|
117
|
+
reviewRequired: z.boolean(),
|
|
118
|
+
outputVerified: z.boolean(),
|
|
119
|
+
output: z.string().nullable(),
|
|
120
|
+
plan: z.string().nullable(),
|
|
121
|
+
failures: z.array(z.unknown()),
|
|
122
|
+
}).passthrough(),
|
|
123
|
+
}).strict();
|
|
70
124
|
}
|
|
71
125
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
id: { type: 'string' },
|
|
86
|
-
kind: { type: 'string' },
|
|
87
|
-
scope: { type: 'string' },
|
|
88
|
-
text: { type: ['string', 'null'] },
|
|
89
|
-
count: { type: 'integer' },
|
|
90
|
-
requiredCardinality: { type: ['string', 'null'] },
|
|
91
|
-
context: { type: ['object', 'null'] },
|
|
92
|
-
allowedActions: { type: 'array', items: { type: 'string' } },
|
|
93
|
-
},
|
|
94
|
-
required: ['id', 'kind', 'scope', 'text', 'count', 'requiredCardinality', 'context', 'allowedActions'],
|
|
95
|
-
additionalProperties: false,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const migrationCatalogOutputSchema = {
|
|
99
|
-
type: 'object',
|
|
100
|
-
properties: {
|
|
101
|
-
tool: { const: 'docx_list_migration_choices' },
|
|
102
|
-
runtime: runtimeIdentitySchema,
|
|
103
|
-
catalog: {
|
|
104
|
-
type: 'object',
|
|
105
|
-
properties: {
|
|
106
|
-
schema: { type: 'string' },
|
|
107
|
-
pass: { type: 'boolean' },
|
|
108
|
-
sourceSha256: { type: 'string' },
|
|
109
|
-
baselineSha256: { type: 'string' },
|
|
110
|
-
sources: { type: 'array', items: migrationChoiceSchema },
|
|
111
|
-
targets: { type: 'array', items: migrationChoiceSchema },
|
|
112
|
-
},
|
|
113
|
-
required: ['schema', 'pass', 'sourceSha256', 'baselineSha256', 'sources', 'targets'],
|
|
114
|
-
additionalProperties: false,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
required: ['tool', 'runtime', 'catalog'],
|
|
118
|
-
additionalProperties: false,
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
function migrationReceiptOutputSchema(tool) {
|
|
122
|
-
return {
|
|
123
|
-
type: 'object',
|
|
124
|
-
properties: {
|
|
125
|
-
tool: { const: tool },
|
|
126
|
-
runtime: runtimeIdentitySchema,
|
|
127
|
-
receipt: {
|
|
128
|
-
type: 'object',
|
|
129
|
-
properties: {
|
|
130
|
-
schema: { type: 'string' },
|
|
131
|
-
toolVersion: { type: 'string' },
|
|
132
|
-
status: { type: 'string', enum: ['pass', 'review-required', 'failed'] },
|
|
133
|
-
pass: { type: 'boolean' },
|
|
134
|
-
reviewRequired: { type: 'boolean' },
|
|
135
|
-
outputVerified: { type: 'boolean' },
|
|
136
|
-
output: { type: ['string', 'null'] },
|
|
137
|
-
plan: { type: ['string', 'null'] },
|
|
138
|
-
failures: { type: 'array', items: { type: 'object' } },
|
|
139
|
-
},
|
|
140
|
-
required: ['schema', 'toolVersion', 'status', 'pass', 'reviewRequired', 'outputVerified', 'output', 'plan', 'failures'],
|
|
141
|
-
additionalProperties: true,
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
required: ['tool', 'runtime', 'receipt'],
|
|
145
|
-
additionalProperties: false,
|
|
146
|
-
};
|
|
126
|
+
const inputOnly = z.object({ input: pathInput }).strict();
|
|
127
|
+
const artifactInput = z.object({
|
|
128
|
+
input: pathInput,
|
|
129
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
130
|
+
}).strict();
|
|
131
|
+
const artifact = z.object({
|
|
132
|
+
path: z.string(),
|
|
133
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
134
|
+
bytes: z.number().int().nonnegative(),
|
|
135
|
+
}).strict();
|
|
136
|
+
|
|
137
|
+
function artifactOutput(tool) {
|
|
138
|
+
return z.object({ tool: z.literal(tool), runtime: runtimeIdentity, artifact }).strict();
|
|
147
139
|
}
|
|
148
140
|
|
|
149
141
|
const tools = [
|
|
150
142
|
{
|
|
151
143
|
name: 'docx_inspect',
|
|
152
|
-
description: 'Inspect a DOCX document and
|
|
153
|
-
inputSchema:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
required: ['input'],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: 'docx_inspect_tables',
|
|
161
|
-
description: 'Inspect DOCX body tables with row, cell, merge, paragraph alignment, run font, color, underline, and text-fill details.',
|
|
162
|
-
inputSchema: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: { input: { type: 'string', description: 'Absolute or relative path to a .docx file.' } },
|
|
165
|
-
required: ['input'],
|
|
166
|
-
},
|
|
144
|
+
description: 'Inspect a DOCX document and write one unified JSON observation containing placeholders, comments, anchors, tables, fields, flow, fonts, and formatting metrics.',
|
|
145
|
+
inputSchema: artifactInput,
|
|
146
|
+
outputSchema: artifactOutput('docx_inspect'),
|
|
147
|
+
handler: docxInspect,
|
|
167
148
|
},
|
|
168
149
|
{
|
|
169
150
|
name: 'docx_list_migration_choices',
|
|
170
151
|
description: 'List every current source item that still needs a business choice and the selectable current baseline targets. Returns opaque ids and context; it does not recommend a choice.',
|
|
171
|
-
inputSchema: {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
additionalProperties: false,
|
|
179
|
-
},
|
|
180
|
-
outputSchema: migrationCatalogOutputSchema,
|
|
152
|
+
inputSchema: z.object({
|
|
153
|
+
source: pathInput.describe('Path to the current source DOCX.'),
|
|
154
|
+
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
155
|
+
}).strict(),
|
|
156
|
+
outputSchema: migrationCatalogOutput,
|
|
157
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
158
|
+
handler: docxListMigrationChoices,
|
|
181
159
|
},
|
|
182
160
|
{
|
|
183
161
|
name: 'docx_migrate_template',
|
|
184
162
|
description: 'Migrate a current DOCX into the selected baseline from one complete batch of business choices. Choices reference only opaque ids returned by docx_list_migration_choices; the tool derives all document values, coordinates, plans, and edits.',
|
|
185
|
-
inputSchema:
|
|
186
|
-
outputSchema:
|
|
163
|
+
inputSchema: templateMigrationInput,
|
|
164
|
+
outputSchema: migrationReceiptOutput('docx_migrate_template'),
|
|
165
|
+
handler: docxMigrateTemplate,
|
|
187
166
|
},
|
|
188
167
|
{
|
|
189
168
|
name: 'docx_verify_migration',
|
|
190
169
|
description: 'Independently re-resolve the same business choices and verify a migrated DOCX against the current source and baseline. This does not trust the migration receipt.',
|
|
191
|
-
inputSchema:
|
|
192
|
-
outputSchema:
|
|
170
|
+
inputSchema: templateMigrationInput,
|
|
171
|
+
outputSchema: migrationReceiptOutput('docx_verify_migration'),
|
|
172
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
173
|
+
handler: docxVerifyMigration,
|
|
193
174
|
},
|
|
194
175
|
{
|
|
195
176
|
name: 'docx_compare',
|
|
196
177
|
description: 'Compare two DOCX files and report package, metric, and style differences.',
|
|
197
|
-
inputSchema: {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
baseline: { type: 'string' },
|
|
201
|
-
updated: { type: 'string' },
|
|
202
|
-
},
|
|
203
|
-
required: ['baseline', 'updated'],
|
|
204
|
-
},
|
|
178
|
+
inputSchema: z.object({ baseline: pathInput, updated: pathInput }).strict(),
|
|
179
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
180
|
+
handler: docxCompare,
|
|
205
181
|
},
|
|
206
182
|
{
|
|
207
183
|
name: 'docx_validate_template_transform',
|
|
208
184
|
description: 'Validate whether a source DOCX template and target DOCX template are structurally compatible.',
|
|
209
|
-
inputSchema: {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
sourceTemplate: { type: 'string' },
|
|
213
|
-
targetTemplate: { type: 'string' },
|
|
214
|
-
},
|
|
215
|
-
required: ['sourceTemplate', 'targetTemplate'],
|
|
216
|
-
},
|
|
185
|
+
inputSchema: z.object({ sourceTemplate: pathInput, targetTemplate: pathInput }).strict(),
|
|
186
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
187
|
+
handler: docxValidateTemplateTransform,
|
|
217
188
|
},
|
|
218
189
|
{
|
|
219
190
|
name: 'docx_export_json',
|
|
220
|
-
description: 'Export
|
|
221
|
-
inputSchema:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
input: { type: 'string' },
|
|
225
|
-
output: { type: 'string' },
|
|
226
|
-
},
|
|
227
|
-
required: ['input'],
|
|
228
|
-
},
|
|
191
|
+
description: 'Export DOCX body content to a new JSON artifact without returning the full document through MCP.',
|
|
192
|
+
inputSchema: artifactInput,
|
|
193
|
+
outputSchema: artifactOutput('docx_export_json'),
|
|
194
|
+
handler: docxExportJson,
|
|
229
195
|
},
|
|
230
196
|
{
|
|
231
197
|
name: 'xlsx_inspect',
|
|
232
|
-
description: 'Inspect an XLSX workbook and
|
|
233
|
-
inputSchema:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
required: ['input'],
|
|
237
|
-
},
|
|
198
|
+
description: 'Inspect an XLSX workbook and write one JSON observation containing workbook structure, exported values, formulas, styles, merged ranges, and conversion evidence.',
|
|
199
|
+
inputSchema: artifactInput,
|
|
200
|
+
outputSchema: artifactOutput('xlsx_inspect'),
|
|
201
|
+
handler: xlsxInspect,
|
|
238
202
|
},
|
|
239
203
|
{
|
|
240
204
|
name: 'xlsx_export_json',
|
|
241
205
|
description: 'Export workbook sheet data from XLSX as structured JSON.',
|
|
242
|
-
inputSchema: {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
required: ['input'],
|
|
250
|
-
},
|
|
206
|
+
inputSchema: z.object({
|
|
207
|
+
input: pathInput,
|
|
208
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
209
|
+
resolveMergedCells: z.boolean().optional().describe('Resolve merged cells to project values.'),
|
|
210
|
+
}).strict(),
|
|
211
|
+
outputSchema: artifactOutput('xlsx_export_json'),
|
|
212
|
+
handler: xlsxExportJson,
|
|
251
213
|
},
|
|
252
214
|
{
|
|
253
215
|
name: 'xlsx_validate',
|
|
254
216
|
description: 'Validate an XLSX workbook package and return Open XML validation evidence.',
|
|
255
|
-
inputSchema:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
required: ['input'],
|
|
259
|
-
},
|
|
217
|
+
inputSchema: inputOnly,
|
|
218
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
219
|
+
handler: xlsxValidate,
|
|
260
220
|
},
|
|
261
221
|
{
|
|
262
222
|
name: 'pptx_inspect',
|
|
263
|
-
description: 'Inspect a PPTX file and
|
|
264
|
-
inputSchema:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
required: ['input'],
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: 'pptx_inspect_detail',
|
|
272
|
-
description: 'Inspect a PPTX file and return detailed slide, shape, transform, paragraph, and run-format evidence.',
|
|
273
|
-
inputSchema: {
|
|
274
|
-
type: 'object',
|
|
275
|
-
properties: { input: { type: 'string' } },
|
|
276
|
-
required: ['input'],
|
|
277
|
-
},
|
|
223
|
+
description: 'Inspect a PPTX file and write one detailed JSON observation containing slides, masters, layouts, shapes, transforms, paragraphs, runs, and placeholders.',
|
|
224
|
+
inputSchema: artifactInput,
|
|
225
|
+
outputSchema: artifactOutput('pptx_inspect'),
|
|
226
|
+
handler: pptxInspect,
|
|
278
227
|
},
|
|
279
228
|
{
|
|
280
229
|
name: 'pptx_export_json',
|
|
281
|
-
description: 'Export PPTX slide text and placeholder hints
|
|
282
|
-
inputSchema:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
input: { type: 'string' },
|
|
286
|
-
output: { type: 'string' },
|
|
287
|
-
},
|
|
288
|
-
required: ['input'],
|
|
289
|
-
},
|
|
230
|
+
description: 'Export PPTX slide text, notes, and placeholder hints to a new JSON artifact without returning the full presentation through MCP.',
|
|
231
|
+
inputSchema: artifactInput,
|
|
232
|
+
outputSchema: artifactOutput('pptx_export_json'),
|
|
233
|
+
handler: pptxExportJson,
|
|
290
234
|
},
|
|
291
235
|
];
|
|
292
236
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
case 'xlsx_inspect':
|
|
312
|
-
return createToolResult(await xlsxInspect(args));
|
|
313
|
-
case 'xlsx_export_json':
|
|
314
|
-
return createToolResult(await xlsxExportJson(args));
|
|
315
|
-
case 'xlsx_validate':
|
|
316
|
-
return createToolResult(await xlsxValidate(args));
|
|
317
|
-
case 'pptx_inspect':
|
|
318
|
-
return createToolResult(await pptxInspect(args));
|
|
319
|
-
case 'pptx_inspect_detail':
|
|
320
|
-
return createToolResult(await pptxInspectDetail(args));
|
|
321
|
-
case 'pptx_export_json':
|
|
322
|
-
return createToolResult(await pptxExportJson(args));
|
|
323
|
-
default:
|
|
324
|
-
throw Object.assign(new Error(`Unknown tool: ${name}`), { code: -32601 });
|
|
237
|
+
function buildServer() {
|
|
238
|
+
const server = new McpServer(
|
|
239
|
+
{ name: 'tiwater-office', version: packageMetadata.version },
|
|
240
|
+
{
|
|
241
|
+
instructions: 'Use the Office tools for technical document observation. For template migration, list the current choices, select only allowed business actions, migrate once, and independently verify the result. Never invent document values, identities, coordinates, plans, or edit operations.',
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
for (const tool of tools) {
|
|
245
|
+
server.registerTool(
|
|
246
|
+
tool.name,
|
|
247
|
+
{
|
|
248
|
+
description: tool.description,
|
|
249
|
+
inputSchema: tool.inputSchema,
|
|
250
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
251
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
252
|
+
},
|
|
253
|
+
async args => createToolResult(await tool.handler(args)),
|
|
254
|
+
);
|
|
325
255
|
}
|
|
256
|
+
return server;
|
|
326
257
|
}
|
|
327
258
|
|
|
328
259
|
async function docxInspect(args) {
|
|
329
260
|
const input = requireString(args.input, 'input');
|
|
330
261
|
const result = await runJsonCandidateChain(docxCandidates, ['inspect', input, '--json']);
|
|
331
|
-
return {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const result = await runJsonCandidateChain(docxCandidates, ['inspect-tables', input, '--json']);
|
|
337
|
-
return { tool: 'docx_inspect_tables', runtime: commandRuntime(result), report: result.json };
|
|
262
|
+
return {
|
|
263
|
+
tool: 'docx_inspect',
|
|
264
|
+
runtime: commandRuntime(result),
|
|
265
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
266
|
+
};
|
|
338
267
|
}
|
|
339
268
|
|
|
340
269
|
async function docxListMigrationChoices(args) {
|
|
@@ -389,19 +318,22 @@ async function docxValidateTemplateTransform(args) {
|
|
|
389
318
|
|
|
390
319
|
async function docxExportJson(args) {
|
|
391
320
|
const input = requireString(args.input, 'input');
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return { tool: 'docx_export_json', runtime: commandRuntime(result), document: JSON.parse(result.stdout) };
|
|
321
|
+
const result = await runJsonCandidateChain(docxCandidates, ['export-json', input]);
|
|
322
|
+
return {
|
|
323
|
+
tool: 'docx_export_json',
|
|
324
|
+
runtime: commandRuntime(result),
|
|
325
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
326
|
+
};
|
|
399
327
|
}
|
|
400
328
|
|
|
401
329
|
async function xlsxInspect(args) {
|
|
402
330
|
const input = requireString(args.input, 'input');
|
|
403
331
|
const result = await runJsonCandidateChain(xlsxCandidates, ['inspect', input, '--json']);
|
|
404
|
-
return {
|
|
332
|
+
return {
|
|
333
|
+
tool: 'xlsx_inspect',
|
|
334
|
+
runtime: commandRuntime(result),
|
|
335
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
336
|
+
};
|
|
405
337
|
}
|
|
406
338
|
|
|
407
339
|
async function xlsxExportJson(args) {
|
|
@@ -410,14 +342,12 @@ async function xlsxExportJson(args) {
|
|
|
410
342
|
if (args.resolveMergedCells) {
|
|
411
343
|
cmdArgs.push('--resolve-merged-cells');
|
|
412
344
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
const result = await runCandidateChain(xlsxCandidates, cmdArgs);
|
|
420
|
-
return { tool: 'xlsx_export_json', runtime: commandRuntime(result), workbook: JSON.parse(result.stdout) };
|
|
345
|
+
const result = await runJsonCandidateChain(xlsxCandidates, cmdArgs);
|
|
346
|
+
return {
|
|
347
|
+
tool: 'xlsx_export_json',
|
|
348
|
+
runtime: commandRuntime(result),
|
|
349
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
350
|
+
};
|
|
421
351
|
}
|
|
422
352
|
|
|
423
353
|
async function xlsxValidate(args) {
|
|
@@ -429,24 +359,33 @@ async function xlsxValidate(args) {
|
|
|
429
359
|
async function pptxInspect(args) {
|
|
430
360
|
const input = requireString(args.input, 'input');
|
|
431
361
|
const result = await runJsonCandidateChain(pptxCandidates, ['inspect', input, '--json']);
|
|
432
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
tool: 'pptx_inspect',
|
|
364
|
+
runtime: commandRuntime(result),
|
|
365
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
366
|
+
};
|
|
433
367
|
}
|
|
434
368
|
|
|
435
|
-
async function
|
|
369
|
+
async function pptxExportJson(args) {
|
|
436
370
|
const input = requireString(args.input, 'input');
|
|
437
|
-
const result = await runJsonCandidateChain(pptxCandidates, ['
|
|
438
|
-
return {
|
|
371
|
+
const result = await runJsonCandidateChain(pptxCandidates, ['export-json', input]);
|
|
372
|
+
return {
|
|
373
|
+
tool: 'pptx_export_json',
|
|
374
|
+
runtime: commandRuntime(result),
|
|
375
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
376
|
+
};
|
|
439
377
|
}
|
|
440
378
|
|
|
441
|
-
async function
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
379
|
+
async function writeJsonArtifact(output, payload) {
|
|
380
|
+
const fullPath = path.resolve(output);
|
|
381
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
382
|
+
const bytes = Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
383
|
+
await writeFile(fullPath, bytes, { flag: 'wx' });
|
|
384
|
+
return {
|
|
385
|
+
path: fullPath,
|
|
386
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
387
|
+
bytes: bytes.length,
|
|
388
|
+
};
|
|
450
389
|
}
|
|
451
390
|
|
|
452
391
|
function commandRuntime(result) {
|
|
@@ -456,7 +395,7 @@ function commandRuntime(result) {
|
|
|
456
395
|
};
|
|
457
396
|
}
|
|
458
397
|
|
|
459
|
-
|
|
398
|
+
serveStdio(buildServer);
|
|
460
399
|
|
|
461
400
|
async function runXlsxValidateCandidateChain(args) {
|
|
462
401
|
const errors = [];
|
|
@@ -487,7 +426,7 @@ async function runXlsxValidateCandidateChain(args) {
|
|
|
487
426
|
|
|
488
427
|
async function runValidationCommand(candidate, args) {
|
|
489
428
|
const env = { ...process.env, ...(candidate.env || {}) };
|
|
490
|
-
const cwd = candidate.cwd ||
|
|
429
|
+
const cwd = candidate.cwd || process.cwd();
|
|
491
430
|
const commandArgs = [...(candidate.argsPrefix || []), ...args];
|
|
492
431
|
|
|
493
432
|
return await new Promise((resolve, reject) => {
|
|
@@ -506,7 +445,7 @@ async function runValidationCommand(candidate, args) {
|
|
|
506
445
|
child.on('error', reject);
|
|
507
446
|
child.on('close', code => {
|
|
508
447
|
if (code === 0 || code === 1) {
|
|
509
|
-
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs });
|
|
448
|
+
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs, cwd });
|
|
510
449
|
return;
|
|
511
450
|
}
|
|
512
451
|
reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiwater/office-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Published MCP server for Tiwater Office document capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
"bin": {
|
|
13
13
|
"tiwater-office-mcp": "office/index.mjs"
|
|
14
14
|
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
15
18
|
"files": [
|
|
16
|
-
"_shared
|
|
19
|
+
"_shared/tool-runtime.mjs",
|
|
17
20
|
"office/index.mjs",
|
|
18
21
|
"office/README.md"
|
|
19
22
|
],
|
|
@@ -22,5 +25,9 @@
|
|
|
22
25
|
},
|
|
23
26
|
"publishConfig": {
|
|
24
27
|
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
31
|
+
"zod": "4.4.3"
|
|
25
32
|
}
|
|
26
33
|
}
|
package/_shared/mcp-stdio.mjs
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import process from 'node:process';
|
|
2
|
-
|
|
3
|
-
const JSONRPC_VERSION = '2.0';
|
|
4
|
-
const SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
|
|
5
|
-
const FALLBACK_PROTOCOL_VERSION = '2025-06-18';
|
|
6
|
-
|
|
7
|
-
function writeMessage(message) {
|
|
8
|
-
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function toError(code, message, data) {
|
|
12
|
-
return { code, message, ...(data === undefined ? {} : { data }) };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizeToolCallError(error) {
|
|
16
|
-
if (!error) return toError(-32603, 'Unknown error');
|
|
17
|
-
if (error.code && error.message) return error;
|
|
18
|
-
return toError(-32603, error instanceof Error ? error.message : String(error));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class McpStdioServer {
|
|
22
|
-
constructor({ name, version, instructions, tools, callTool, logger = console.error }) {
|
|
23
|
-
this.serverInfo = { name, version };
|
|
24
|
-
this.instructions = instructions;
|
|
25
|
-
this.tools = tools;
|
|
26
|
-
this.callTool = callTool;
|
|
27
|
-
this.logger = logger;
|
|
28
|
-
this.lineBuffer = '';
|
|
29
|
-
this.binaryBuffer = Buffer.alloc(0);
|
|
30
|
-
this.initialized = false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
start() {
|
|
34
|
-
process.stdin.on('data', chunk => this.#onData(chunk));
|
|
35
|
-
process.stdin.on('end', () => process.exit(0));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#onData(chunk) {
|
|
39
|
-
const text = chunk.toString('utf8');
|
|
40
|
-
|
|
41
|
-
if (this.binaryBuffer.length > 0 || text.includes('Content-Length:')) {
|
|
42
|
-
this.binaryBuffer = Buffer.concat([this.binaryBuffer, chunk]);
|
|
43
|
-
this.#drainContentLengthBuffer();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
this.lineBuffer += text;
|
|
48
|
-
while (true) {
|
|
49
|
-
const newlineIndex = this.lineBuffer.indexOf('\n');
|
|
50
|
-
if (newlineIndex === -1) return;
|
|
51
|
-
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, '').trim();
|
|
52
|
-
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
53
|
-
if (!line) continue;
|
|
54
|
-
this.#parseAndHandle(line, null);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
#drainContentLengthBuffer() {
|
|
59
|
-
while (true) {
|
|
60
|
-
const headerEnd = this.binaryBuffer.indexOf('\r\n\r\n');
|
|
61
|
-
if (headerEnd === -1) return;
|
|
62
|
-
|
|
63
|
-
const headerText = this.binaryBuffer.subarray(0, headerEnd).toString('utf8');
|
|
64
|
-
const lengthMatch = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
65
|
-
if (!lengthMatch) {
|
|
66
|
-
this.logger('Missing Content-Length header');
|
|
67
|
-
this.binaryBuffer = Buffer.alloc(0);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const contentLength = Number(lengthMatch[1]);
|
|
72
|
-
const messageStart = headerEnd + 4;
|
|
73
|
-
const messageEnd = messageStart + contentLength;
|
|
74
|
-
if (this.binaryBuffer.length < messageEnd) return;
|
|
75
|
-
|
|
76
|
-
const body = this.binaryBuffer.subarray(messageStart, messageEnd).toString('utf8');
|
|
77
|
-
this.binaryBuffer = this.binaryBuffer.subarray(messageEnd);
|
|
78
|
-
this.#parseAndHandle(body, null);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
#parseAndHandle(body, idHint) {
|
|
83
|
-
let message;
|
|
84
|
-
try {
|
|
85
|
-
message = JSON.parse(body);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
writeMessage({ jsonrpc: JSONRPC_VERSION, id: idHint, error: toError(-32700, 'Parse error', String(error)) });
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
void this.#handleMessage(message);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async #handleMessage(message) {
|
|
95
|
-
if (!message || message.jsonrpc !== JSONRPC_VERSION || typeof message.method !== 'string') {
|
|
96
|
-
if ('id' in (message || {})) {
|
|
97
|
-
writeMessage({ jsonrpc: JSONRPC_VERSION, id: message.id ?? null, error: toError(-32600, 'Invalid Request') });
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const { id, method, params = {} } = message;
|
|
103
|
-
const isNotification = id === undefined;
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
switch (method) {
|
|
107
|
-
case 'initialize': {
|
|
108
|
-
const requested = params.protocolVersion;
|
|
109
|
-
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : FALLBACK_PROTOCOL_VERSION;
|
|
110
|
-
const result = {
|
|
111
|
-
protocolVersion,
|
|
112
|
-
capabilities: { tools: {} },
|
|
113
|
-
serverInfo: this.serverInfo,
|
|
114
|
-
...(this.instructions ? { instructions: this.instructions } : {}),
|
|
115
|
-
};
|
|
116
|
-
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
case 'notifications/initialized': {
|
|
120
|
-
this.initialized = true;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
case 'ping': {
|
|
124
|
-
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result: {} });
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
case 'tools/list': {
|
|
128
|
-
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result: { tools: this.tools } });
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
case 'tools/call': {
|
|
132
|
-
const name = params?.name;
|
|
133
|
-
const args = params?.arguments ?? {};
|
|
134
|
-
if (typeof name !== 'string' || !name) {
|
|
135
|
-
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: toError(-32602, 'Invalid params: missing tool name') });
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const result = await this.callTool(name, args);
|
|
139
|
-
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
default: {
|
|
143
|
-
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: toError(-32601, `Method not found: ${method}`) });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (!isNotification) {
|
|
148
|
-
writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: normalizeToolCallError(error) });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|