@tiwater/office-mcp 0.4.0 → 0.6.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 +5 -0
- package/office/index.mjs +191 -2
- package/package.json +1 -1
package/office/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Shared stdio MCP server for Office document workflows.
|
|
|
6
6
|
|
|
7
7
|
- `docx_inspect`
|
|
8
8
|
- `docx_list_migration_choices`
|
|
9
|
+
- `docx_query_migration_choices`
|
|
9
10
|
- `docx_migrate_template`
|
|
10
11
|
- `docx_verify_migration`
|
|
11
12
|
- `docx_compare`
|
|
@@ -30,3 +31,7 @@ The official MCP SDK derives the schemas advertised to clients and validates
|
|
|
30
31
|
tool arguments and structured results before they cross the protocol boundary.
|
|
31
32
|
Large observations and exports are written to a caller-selected new JSON
|
|
32
33
|
artifact. MCP returns only the artifact path, hash, and byte count.
|
|
34
|
+
Template-migration choice artifacts are opaque evidence. Query the same current
|
|
35
|
+
source and baseline through `docx_query_migration_choices` to page unresolved
|
|
36
|
+
sources, request targets compatible with one business action, or inspect cleanup
|
|
37
|
+
targets.
|
package/office/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
7
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
7
8
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
8
9
|
import * as z from 'zod/v4';
|
|
@@ -100,7 +101,17 @@ const migrationCatalog = z.object({
|
|
|
100
101
|
baselineSha256: z.string(),
|
|
101
102
|
sources: z.array(migrationChoiceOutput),
|
|
102
103
|
targets: z.array(migrationChoiceOutput),
|
|
103
|
-
}).strict()
|
|
104
|
+
}).strict().superRefine((catalog, context) => {
|
|
105
|
+
for (const key of ['sources', 'targets']) {
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (const [index, choice] of catalog[key].entries()) {
|
|
108
|
+
if (seen.has(choice.id)) {
|
|
109
|
+
context.addIssue({ code: 'custom', path: [key, index, 'id'], message: `duplicate ${key} choice id: ${choice.id}` });
|
|
110
|
+
}
|
|
111
|
+
seen.add(choice.id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
104
115
|
|
|
105
116
|
const migrationReceipt = z.object({
|
|
106
117
|
schema: z.string(),
|
|
@@ -139,6 +150,76 @@ const migrationCatalogOutput = z.object({
|
|
|
139
150
|
}).strict(),
|
|
140
151
|
}).strict();
|
|
141
152
|
|
|
153
|
+
const migrationQueryPage = z.object({
|
|
154
|
+
offset: z.number().int().nonnegative(),
|
|
155
|
+
returned: z.number().int().nonnegative(),
|
|
156
|
+
total: z.number().int().nonnegative(),
|
|
157
|
+
hasMore: z.boolean(),
|
|
158
|
+
}).strict();
|
|
159
|
+
|
|
160
|
+
const migrationTargetPage = z.object({
|
|
161
|
+
schema: z.string(),
|
|
162
|
+
pass: z.boolean(),
|
|
163
|
+
sourceChoiceId: z.string().nullable(),
|
|
164
|
+
branch: z.string(),
|
|
165
|
+
offset: z.number().int().nonnegative(),
|
|
166
|
+
limit: z.number().int().positive(),
|
|
167
|
+
total: z.number().int().nonnegative(),
|
|
168
|
+
targets: z.array(migrationChoiceOutput),
|
|
169
|
+
}).strict();
|
|
170
|
+
|
|
171
|
+
const migrationTargetAction = z.enum([
|
|
172
|
+
'place-content',
|
|
173
|
+
'keep-template-content',
|
|
174
|
+
'keep-template-label',
|
|
175
|
+
'select-template-option',
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const migrationQueryDocuments = {
|
|
179
|
+
source: pathInput.describe('Path to the current source DOCX used by docx_list_migration_choices.'),
|
|
180
|
+
baseline: pathInput.describe('Path to the same selected current baseline DOCX used by docx_list_migration_choices.'),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const boundedOffset = z.number().int().nonnegative().optional().describe('Zero-based result offset. Defaults to 0.');
|
|
184
|
+
const boundedLimit = z.number().int().min(1).max(10).optional().describe('Maximum results to return. Defaults to 10 and cannot exceed 10.');
|
|
185
|
+
|
|
186
|
+
const migrationChoiceQueryInput = z.discriminatedUnion('view', [
|
|
187
|
+
z.object({
|
|
188
|
+
...migrationQueryDocuments,
|
|
189
|
+
view: z.literal('sources'),
|
|
190
|
+
offset: boundedOffset,
|
|
191
|
+
limit: boundedLimit,
|
|
192
|
+
}).strict(),
|
|
193
|
+
z.object({
|
|
194
|
+
...migrationQueryDocuments,
|
|
195
|
+
view: z.literal('targets'),
|
|
196
|
+
sourceChoiceId: z.string().trim().min(1).describe('Opaque current source id returned by the sources view.'),
|
|
197
|
+
action: migrationTargetAction.describe('Business action whose technically compatible current baseline targets are requested.'),
|
|
198
|
+
text: z.string().trim().min(1).optional().describe('Optional literal case-insensitive text to find in target visible text or context.'),
|
|
199
|
+
offset: boundedOffset,
|
|
200
|
+
limit: boundedLimit,
|
|
201
|
+
}).strict(),
|
|
202
|
+
z.object({
|
|
203
|
+
...migrationQueryDocuments,
|
|
204
|
+
view: z.literal('cleanup'),
|
|
205
|
+
text: z.string().trim().min(1).optional().describe('Optional literal case-insensitive text to find in cleanup target visible text or context.'),
|
|
206
|
+
offset: boundedOffset,
|
|
207
|
+
limit: boundedLimit,
|
|
208
|
+
}).strict(),
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
const migrationChoiceQueryOutput = z.object({
|
|
212
|
+
tool: z.literal('docx_query_migration_choices'),
|
|
213
|
+
runtime: runtimeIdentity,
|
|
214
|
+
sourceSha256: z.string(),
|
|
215
|
+
baselineSha256: z.string(),
|
|
216
|
+
view: z.enum(['sources', 'targets', 'cleanup']),
|
|
217
|
+
action: migrationTargetAction.nullable(),
|
|
218
|
+
source: migrationChoiceOutput.nullable(),
|
|
219
|
+
items: z.array(migrationChoiceOutput),
|
|
220
|
+
page: migrationQueryPage,
|
|
221
|
+
}).strict();
|
|
222
|
+
|
|
142
223
|
function migrationReceiptOutput(tool) {
|
|
143
224
|
return z.object({
|
|
144
225
|
tool: z.literal(tool),
|
|
@@ -172,7 +253,7 @@ const tools = [
|
|
|
172
253
|
},
|
|
173
254
|
{
|
|
174
255
|
name: 'docx_list_migration_choices',
|
|
175
|
-
description: 'Write every current source item that still needs a business choice and the selectable current baseline targets to
|
|
256
|
+
description: 'Write every current source item that still needs a business choice and the selectable current baseline targets to an opaque run-local evidence artifact. Use docx_query_migration_choices with the same source and baseline to inspect bounded alternatives; do not parse the artifact.',
|
|
176
257
|
inputSchema: z.object({
|
|
177
258
|
source: pathInput.describe('Path to the current source DOCX.'),
|
|
178
259
|
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
@@ -181,6 +262,14 @@ const tools = [
|
|
|
181
262
|
outputSchema: migrationCatalogOutput,
|
|
182
263
|
handler: docxListMigrationChoices,
|
|
183
264
|
},
|
|
265
|
+
{
|
|
266
|
+
name: 'docx_query_migration_choices',
|
|
267
|
+
description: 'Query current template-migration alternatives without reading the catalog artifact. Page unresolved sources, request provider-compatible targets for one source and business action, or inspect cleanup targets. This tool does not rank, recommend, or make a business choice.',
|
|
268
|
+
inputSchema: migrationChoiceQueryInput,
|
|
269
|
+
outputSchema: migrationChoiceQueryOutput,
|
|
270
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
271
|
+
handler: docxQueryMigrationChoices,
|
|
272
|
+
},
|
|
184
273
|
{
|
|
185
274
|
name: 'docx_migrate_template',
|
|
186
275
|
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.',
|
|
@@ -309,6 +398,106 @@ async function docxListMigrationChoices(args) {
|
|
|
309
398
|
};
|
|
310
399
|
}
|
|
311
400
|
|
|
401
|
+
async function docxQueryMigrationChoices(args) {
|
|
402
|
+
const sourcePath = requireString(args.source, 'source');
|
|
403
|
+
const baselinePath = requireString(args.baseline, 'baseline');
|
|
404
|
+
const offset = args.offset ?? 0;
|
|
405
|
+
const limit = args.limit ?? 10;
|
|
406
|
+
const catalogResult = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', sourcePath, baselinePath]);
|
|
407
|
+
const catalog = migrationCatalog.parse(catalogResult.json);
|
|
408
|
+
|
|
409
|
+
if (args.view === 'sources') {
|
|
410
|
+
return migrationQueryResult({
|
|
411
|
+
runtime: commandRuntime(catalogResult),
|
|
412
|
+
catalog,
|
|
413
|
+
view: 'sources',
|
|
414
|
+
action: null,
|
|
415
|
+
source: null,
|
|
416
|
+
items: catalog.sources.slice(offset, offset + limit),
|
|
417
|
+
offset,
|
|
418
|
+
total: catalog.sources.length,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let source = null;
|
|
423
|
+
let action = null;
|
|
424
|
+
let branch;
|
|
425
|
+
let sourceChoiceId = '-';
|
|
426
|
+
if (args.view === 'cleanup') {
|
|
427
|
+
branch = 'baseline-clear';
|
|
428
|
+
} else {
|
|
429
|
+
source = catalog.sources.find(item => item.id === args.sourceChoiceId) ?? null;
|
|
430
|
+
if (!source) {
|
|
431
|
+
throw Object.assign(new Error(`Unknown sourceChoiceId: ${args.sourceChoiceId}`), { code: -32602 });
|
|
432
|
+
}
|
|
433
|
+
action = args.action;
|
|
434
|
+
if (!source.allowedActions.includes(action)) {
|
|
435
|
+
throw Object.assign(new Error(`Action ${action} is not allowed for ${source.id}`), { code: -32602 });
|
|
436
|
+
}
|
|
437
|
+
sourceChoiceId = source.id;
|
|
438
|
+
branch = migrationTargetBranch(action, source.kind);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const targetResult = await runJsonCandidateChain(docxCandidates, [
|
|
442
|
+
'find-template-migration-targets',
|
|
443
|
+
sourcePath,
|
|
444
|
+
baselinePath,
|
|
445
|
+
sourceChoiceId,
|
|
446
|
+
branch,
|
|
447
|
+
args.text ?? '-',
|
|
448
|
+
String(offset),
|
|
449
|
+
String(limit),
|
|
450
|
+
]);
|
|
451
|
+
const targetPage = migrationTargetPage.parse(targetResult.json);
|
|
452
|
+
if (targetPage.sourceChoiceId !== (args.view === 'cleanup' ? null : sourceChoiceId) || targetPage.branch !== branch) {
|
|
453
|
+
throw new Error('Migration target page identity does not match the requested current source and action');
|
|
454
|
+
}
|
|
455
|
+
const catalogTargets = new Map(catalog.targets.map(item => [item.id, item]));
|
|
456
|
+
for (const target of targetPage.targets) {
|
|
457
|
+
const current = catalogTargets.get(target.id);
|
|
458
|
+
if (!current || !isDeepStrictEqual(current, target)) {
|
|
459
|
+
throw new Error(`Migration target ${target.id} is not bound to the current catalog`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return migrationQueryResult({
|
|
463
|
+
runtime: commandRuntime(targetResult),
|
|
464
|
+
catalog,
|
|
465
|
+
view: args.view,
|
|
466
|
+
action,
|
|
467
|
+
source,
|
|
468
|
+
items: targetPage.targets,
|
|
469
|
+
offset: targetPage.offset,
|
|
470
|
+
total: targetPage.total,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function migrationTargetBranch(action, sourceKind) {
|
|
475
|
+
if (action === 'place-content') return sourceKind === 'media' ? 'copy-media' : 'copy-text';
|
|
476
|
+
if (action === 'keep-template-content') return 'retain-target';
|
|
477
|
+
if (action === 'keep-template-label') return 'retain-target-label';
|
|
478
|
+
if (action === 'select-template-option') return 'choice-selection';
|
|
479
|
+
throw new Error(`Unsupported migration target action: ${action}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function migrationQueryResult({ runtime, catalog, view, action, source, items, offset, total }) {
|
|
483
|
+
return {
|
|
484
|
+
tool: 'docx_query_migration_choices',
|
|
485
|
+
runtime,
|
|
486
|
+
sourceSha256: catalog.sourceSha256,
|
|
487
|
+
baselineSha256: catalog.baselineSha256,
|
|
488
|
+
view,
|
|
489
|
+
action,
|
|
490
|
+
source,
|
|
491
|
+
items,
|
|
492
|
+
page: {
|
|
493
|
+
offset,
|
|
494
|
+
returned: items.length,
|
|
495
|
+
total,
|
|
496
|
+
hasMore: offset + items.length < total,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
312
501
|
async function docxMigrateTemplate(args) {
|
|
313
502
|
return runTemplateMigrationCommand('docx_migrate_template', 'migrate-template', args);
|
|
314
503
|
}
|