@tiwater/office-mcp 0.3.0 → 0.4.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.
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
@@ -78,7 +79,7 @@ export function requireString(value, label) {
78
79
  }
79
80
 
80
81
  async function runCommand(candidate, args, options) {
81
- const env = { ...process.env, ...(candidate.env || {}), ...(options.env || {}) };
82
+ const env = await withDotnetRoot({ ...process.env, ...(candidate.env || {}), ...(options.env || {}) });
82
83
  const cwd = candidate.cwd || options.cwd || repoRoot;
83
84
  const commandArgs = [...(candidate.argsPrefix || []), ...args];
84
85
 
@@ -106,3 +107,35 @@ async function runCommand(candidate, args, options) {
106
107
  });
107
108
  });
108
109
  }
110
+
111
+ async function withDotnetRoot(env) {
112
+ const architectureVariable = process.arch === 'arm64'
113
+ ? 'DOTNET_ROOT_ARM64'
114
+ : process.arch === 'x64'
115
+ ? 'DOTNET_ROOT_X64'
116
+ : null;
117
+ if (env.DOTNET_ROOT || (architectureVariable && env[architectureVariable])) return env;
118
+
119
+ const dotnet = await findOnPath(process.platform === 'win32' ? 'dotnet.exe' : 'dotnet', env.PATH);
120
+ if (!dotnet) return env;
121
+
122
+ const root = path.dirname(dotnet);
123
+ return {
124
+ ...env,
125
+ DOTNET_ROOT: root,
126
+ ...(architectureVariable ? { [architectureVariable]: root } : {}),
127
+ };
128
+ }
129
+
130
+ async function findOnPath(command, pathValue) {
131
+ for (const directory of String(pathValue ?? '').split(path.delimiter).filter(Boolean)) {
132
+ const candidate = path.join(directory, command);
133
+ try {
134
+ await fs.access(candidate, fsConstants.X_OK);
135
+ return candidate;
136
+ } catch {
137
+ // Continue to the next PATH entry.
138
+ }
139
+ }
140
+ return null;
141
+ }
package/office/index.mjs CHANGED
@@ -72,6 +72,7 @@ const templateMigrationInput = z.object({
72
72
  source: pathInput.describe('Path to the current source DOCX.'),
73
73
  baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
74
74
  output: pathInput.describe('Path to the migrated output DOCX.'),
75
+ receiptOutput: pathInput.describe('New JSON receipt artifact path. Existing files are never overwritten.'),
75
76
  choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source id returned by docx_list_migration_choices.'),
76
77
  templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
77
78
  }).strict();
@@ -92,16 +93,49 @@ const migrationChoiceOutput = z.object({
92
93
  allowedActions: z.array(z.string()),
93
94
  }).strict();
94
95
 
96
+ const migrationCatalog = z.object({
97
+ schema: z.string(),
98
+ pass: z.boolean(),
99
+ sourceSha256: z.string(),
100
+ baselineSha256: z.string(),
101
+ sources: z.array(migrationChoiceOutput),
102
+ targets: z.array(migrationChoiceOutput),
103
+ }).strict();
104
+
105
+ const migrationReceipt = z.object({
106
+ schema: z.string(),
107
+ toolVersion: z.string(),
108
+ status: z.enum(['pass', 'review-required', 'failed']),
109
+ pass: z.boolean(),
110
+ reviewRequired: z.boolean(),
111
+ outputVerified: z.boolean(),
112
+ output: z.string().nullable(),
113
+ plan: z.string().nullable(),
114
+ failures: z.array(z.unknown()),
115
+ }).passthrough();
116
+
117
+ const inputOnly = z.object({ input: pathInput }).strict();
118
+ const artifactInput = z.object({
119
+ input: pathInput,
120
+ output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
121
+ }).strict();
122
+ const artifact = z.object({
123
+ path: z.string(),
124
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
125
+ bytes: z.number().int().nonnegative(),
126
+ }).strict();
127
+
95
128
  const migrationCatalogOutput = z.object({
96
129
  tool: z.literal('docx_list_migration_choices'),
97
130
  runtime: runtimeIdentity,
98
- catalog: z.object({
131
+ artifact,
132
+ summary: z.object({
99
133
  schema: z.string(),
100
134
  pass: z.boolean(),
101
135
  sourceSha256: z.string(),
102
136
  baselineSha256: z.string(),
103
- sources: z.array(migrationChoiceOutput),
104
- targets: z.array(migrationChoiceOutput),
137
+ sourceCount: z.number().int().nonnegative(),
138
+ targetCount: z.number().int().nonnegative(),
105
139
  }).strict(),
106
140
  }).strict();
107
141
 
@@ -109,7 +143,8 @@ function migrationReceiptOutput(tool) {
109
143
  return z.object({
110
144
  tool: z.literal(tool),
111
145
  runtime: runtimeIdentity,
112
- receipt: z.object({
146
+ artifact,
147
+ summary: z.object({
113
148
  schema: z.string(),
114
149
  toolVersion: z.string(),
115
150
  status: z.enum(['pass', 'review-required', 'failed']),
@@ -118,22 +153,11 @@ function migrationReceiptOutput(tool) {
118
153
  outputVerified: z.boolean(),
119
154
  output: z.string().nullable(),
120
155
  plan: z.string().nullable(),
121
- failures: z.array(z.unknown()),
122
- }).passthrough(),
156
+ failureCount: z.number().int().nonnegative(),
157
+ }).strict(),
123
158
  }).strict();
124
159
  }
125
160
 
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
161
  function artifactOutput(tool) {
138
162
  return z.object({ tool: z.literal(tool), runtime: runtimeIdentity, artifact }).strict();
139
163
  }
@@ -148,13 +172,13 @@ const tools = [
148
172
  },
149
173
  {
150
174
  name: 'docx_list_migration_choices',
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.',
175
+ description: 'Write every current source item that still needs a business choice and the selectable current baseline targets to a run-local JSON artifact. Returns only artifact metadata and counts; it does not recommend a choice.',
152
176
  inputSchema: z.object({
153
177
  source: pathInput.describe('Path to the current source DOCX.'),
154
178
  baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
179
+ output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
155
180
  }).strict(),
156
181
  outputSchema: migrationCatalogOutput,
157
- annotations: { readOnlyHint: true, idempotentHint: true },
158
182
  handler: docxListMigrationChoices,
159
183
  },
160
184
  {
@@ -169,7 +193,6 @@ const tools = [
169
193
  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.',
170
194
  inputSchema: templateMigrationInput,
171
195
  outputSchema: migrationReceiptOutput('docx_verify_migration'),
172
- annotations: { readOnlyHint: true, idempotentHint: true },
173
196
  handler: docxVerifyMigration,
174
197
  },
175
198
  {
@@ -270,7 +293,20 @@ async function docxListMigrationChoices(args) {
270
293
  const source = requireString(args.source, 'source');
271
294
  const baseline = requireString(args.baseline, 'baseline');
272
295
  const result = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', source, baseline]);
273
- return { tool: 'docx_list_migration_choices', runtime: commandRuntime(result), catalog: result.json };
296
+ const catalog = migrationCatalog.parse(result.json);
297
+ return {
298
+ tool: 'docx_list_migration_choices',
299
+ runtime: commandRuntime(result),
300
+ artifact: await writeJsonArtifact(requireString(args.output, 'output'), catalog),
301
+ summary: {
302
+ schema: catalog.schema,
303
+ pass: catalog.pass,
304
+ sourceSha256: catalog.sourceSha256,
305
+ baselineSha256: catalog.baselineSha256,
306
+ sourceCount: catalog.sources.length,
307
+ targetCount: catalog.targets.length,
308
+ },
309
+ };
274
310
  }
275
311
 
276
312
  async function docxMigrateTemplate(args) {
@@ -298,7 +334,23 @@ async function runTemplateMigrationCommand(tool, command, args) {
298
334
  docxCandidates,
299
335
  [command, source, baseline, choicesPath, output],
300
336
  { allowedExitCodes: [0, 1] });
301
- return { tool, runtime: commandRuntime(result), receipt: result.json };
337
+ const receipt = migrationReceipt.parse(result.json);
338
+ return {
339
+ tool,
340
+ runtime: commandRuntime(result),
341
+ artifact: await writeJsonArtifact(requireString(args.receiptOutput, 'receiptOutput'), receipt),
342
+ summary: {
343
+ schema: receipt.schema,
344
+ toolVersion: receipt.toolVersion,
345
+ status: receipt.status,
346
+ pass: receipt.pass,
347
+ reviewRequired: receipt.reviewRequired,
348
+ outputVerified: receipt.outputVerified,
349
+ output: receipt.output,
350
+ plan: receipt.plan,
351
+ failureCount: receipt.failures.length,
352
+ },
353
+ };
302
354
  });
303
355
  }
304
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",