@tiwater/office-mcp 0.4.0 → 0.5.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
@@ -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`
package/office/index.mjs CHANGED
@@ -100,7 +100,17 @@ const migrationCatalog = z.object({
100
100
  baselineSha256: z.string(),
101
101
  sources: z.array(migrationChoiceOutput),
102
102
  targets: z.array(migrationChoiceOutput),
103
- }).strict();
103
+ }).strict().superRefine((catalog, context) => {
104
+ for (const key of ['sources', 'targets']) {
105
+ const seen = new Set();
106
+ for (const [index, choice] of catalog[key].entries()) {
107
+ if (seen.has(choice.id)) {
108
+ context.addIssue({ code: 'custom', path: [key, index, 'id'], message: `duplicate ${key} choice id: ${choice.id}` });
109
+ }
110
+ seen.add(choice.id);
111
+ }
112
+ }
113
+ });
104
114
 
105
115
  const migrationReceipt = z.object({
106
116
  schema: z.string(),
@@ -139,6 +149,41 @@ const migrationCatalogOutput = z.object({
139
149
  }).strict(),
140
150
  }).strict();
141
151
 
152
+ const migrationQueryPage = z.object({
153
+ offset: z.number().int().nonnegative(),
154
+ returned: z.number().int().nonnegative(),
155
+ total: z.number().int().nonnegative(),
156
+ hasMore: z.boolean(),
157
+ }).strict();
158
+
159
+ const migrationChoiceQueryInput = z.discriminatedUnion('view', [
160
+ z.object({
161
+ catalog: pathInput.describe('Path returned by docx_list_migration_choices.'),
162
+ view: z.literal('sources'),
163
+ offset: z.number().int().nonnegative().optional(),
164
+ limit: z.number().int().min(1).max(10).optional(),
165
+ }).strict(),
166
+ z.object({
167
+ catalog: pathInput.describe('Path returned by docx_list_migration_choices.'),
168
+ view: z.literal('targets'),
169
+ sourceChoiceId: z.string().trim().min(1),
170
+ text: z.string().trim().min(1).optional().describe('Literal case-insensitive text to find in target text or visible context.'),
171
+ kinds: z.array(z.string().trim().min(1)).min(1).optional(),
172
+ scopes: z.array(z.string().trim().min(1)).min(1).optional(),
173
+ offset: z.number().int().nonnegative().optional(),
174
+ limit: z.number().int().min(1).max(10).optional(),
175
+ }).strict(),
176
+ ]);
177
+
178
+ const migrationChoiceQueryOutput = z.object({
179
+ tool: z.literal('docx_query_migration_choices'),
180
+ catalogSha256: z.string().regex(/^[0-9a-f]{64}$/),
181
+ view: z.enum(['sources', 'targets']),
182
+ source: migrationChoiceOutput.nullable(),
183
+ items: z.array(migrationChoiceOutput),
184
+ page: migrationQueryPage,
185
+ }).strict();
186
+
142
187
  function migrationReceiptOutput(tool) {
143
188
  return z.object({
144
189
  tool: z.literal(tool),
@@ -181,6 +226,14 @@ const tools = [
181
226
  outputSchema: migrationCatalogOutput,
182
227
  handler: docxListMigrationChoices,
183
228
  },
229
+ {
230
+ name: 'docx_query_migration_choices',
231
+ description: 'Read one bounded page from a migration-choice catalog. List source choices, or inspect targets for one source using literal text, kind, and scope filters. This tool does not recommend or make a business choice.',
232
+ inputSchema: migrationChoiceQueryInput,
233
+ outputSchema: migrationChoiceQueryOutput,
234
+ annotations: { readOnlyHint: true, idempotentHint: true },
235
+ handler: docxQueryMigrationChoices,
236
+ },
184
237
  {
185
238
  name: 'docx_migrate_template',
186
239
  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 +362,62 @@ async function docxListMigrationChoices(args) {
309
362
  };
310
363
  }
311
364
 
365
+ async function docxQueryMigrationChoices(args) {
366
+ const catalogPath = requireString(args.catalog, 'catalog');
367
+ const bytes = await readFile(catalogPath);
368
+ const catalog = migrationCatalog.parse(JSON.parse(bytes.toString('utf8')));
369
+ const offset = args.offset ?? 0;
370
+ const limit = args.limit ?? 10;
371
+ let source = null;
372
+ let matches;
373
+
374
+ if (args.view === 'sources') {
375
+ matches = catalog.sources;
376
+ } else {
377
+ source = catalog.sources.find(item => item.id === args.sourceChoiceId) ?? null;
378
+ if (!source) {
379
+ throw Object.assign(new Error(`Unknown sourceChoiceId: ${args.sourceChoiceId}`), { code: -32602 });
380
+ }
381
+ const kinds = args.kinds ? new Set(args.kinds) : null;
382
+ const scopes = args.scopes ? new Set(args.scopes) : null;
383
+ const textQuery = args.text?.toLocaleLowerCase();
384
+ matches = catalog.targets.filter(item =>
385
+ (!kinds || kinds.has(item.kind)) &&
386
+ (!scopes || scopes.has(item.scope)) &&
387
+ (!textQuery || migrationChoiceSearchText(item).includes(textQuery)));
388
+ }
389
+
390
+ const items = matches.slice(offset, offset + limit);
391
+ return {
392
+ tool: 'docx_query_migration_choices',
393
+ catalogSha256: createHash('sha256').update(bytes).digest('hex'),
394
+ view: args.view,
395
+ source,
396
+ items,
397
+ page: {
398
+ offset,
399
+ returned: items.length,
400
+ total: matches.length,
401
+ hasMore: offset + items.length < matches.length,
402
+ },
403
+ };
404
+ }
405
+
406
+ function migrationChoiceSearchText(choice) {
407
+ return collectVisibleStrings({ text: choice.text, context: choice.context })
408
+ .join('\n')
409
+ .toLocaleLowerCase();
410
+ }
411
+
412
+ function collectVisibleStrings(value, fieldName = '') {
413
+ if (typeof value === 'string') return /text/i.test(fieldName) ? [value] : [];
414
+ if (Array.isArray(value)) return value.flatMap(item => collectVisibleStrings(item, fieldName));
415
+ if (value && typeof value === 'object') {
416
+ return Object.entries(value).flatMap(([key, item]) => collectVisibleStrings(item, key));
417
+ }
418
+ return [];
419
+ }
420
+
312
421
  async function docxMigrateTemplate(args) {
313
422
  return runTemplateMigrationCommand('docx_migrate_template', 'migrate-template', args);
314
423
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",