@tiwater/office-mcp 0.21.0 → 0.21.2

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.
@@ -16,7 +16,7 @@ export function resultChannels(args) {
16
16
  return { returnContent, output };
17
17
  }
18
18
 
19
- export async function deliverLargeJsonResult({ tool, args, runtime, payload, sourcePaths }) {
19
+ export async function deliverLargeJsonResult({ tool, args, runtime, payload, sourcePaths, summary }) {
20
20
  const channels = resultChannels(args);
21
21
  const contentBytes = Buffer.byteLength(JSON.stringify(payload), 'utf8');
22
22
  const contentReturned = channels.returnContent && contentBytes <= returnedContentBudgetBytes;
@@ -37,6 +37,7 @@ export async function deliverLargeJsonResult({ tool, args, runtime, payload, sou
37
37
  contentReturned,
38
38
  contentWritten: channels.output !== null,
39
39
  },
40
+ ...(summary === undefined ? {} : { summary }),
40
41
  ...(contentReturned ? { content: payload } : {}),
41
42
  };
42
43
  }
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+
3
+ const pendingWrites = new Map();
4
+
5
+ export async function withOutputWriteLock(output, write) {
6
+ const target = path.resolve(output);
7
+ const previous = pendingWrites.get(target) ?? Promise.resolve();
8
+ const current = previous.catch(() => undefined).then(write);
9
+ pendingWrites.set(target, current);
10
+ try {
11
+ return await current;
12
+ } finally {
13
+ if (pendingWrites.get(target) === current) pendingWrites.delete(target);
14
+ }
15
+ }
@@ -2,7 +2,7 @@
2
2
  "schema": "tiwater.office-provider-contract-manifest/v1",
3
3
  "provider": {
4
4
  "id": "@tiwater/office-mcp",
5
- "version": "0.21.0"
5
+ "version": "0.21.2"
6
6
  },
7
7
  "tools": [
8
8
  {
package/office/index.mjs CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  returnedContentBudgetBytes,
22
22
  writeJsonArtifact,
23
23
  } from '../_shared/large-json-result.mjs';
24
+ import { withOutputWriteLock } from '../_shared/output-write-lock.mjs';
24
25
 
25
26
  const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
26
27
  const inputContractManifest = JSON.parse(await readFile(
@@ -179,6 +180,39 @@ function largeResultOutput(tool) {
179
180
  }).strict();
180
181
  }
181
182
 
183
+ function largeInspectionOutput(tool, summary) {
184
+ return largeResultOutput(tool).extend({ summary }).strict();
185
+ }
186
+
187
+ const xlsxInspectionSummary = z.object({
188
+ sheetCount: z.number().int().nonnegative(),
189
+ sheets: z.array(z.object({
190
+ name: z.string(),
191
+ rowCount: z.number().int().nonnegative(),
192
+ columnCount: z.number().int().nonnegative(),
193
+ usedRange: z.string().nullable(),
194
+ mergedRangeCount: z.number().int().nonnegative(),
195
+ formulaCellCount: z.number().int().nonnegative(),
196
+ openingText: z.array(z.object({
197
+ reference: z.string(),
198
+ textPreview: z.string(),
199
+ }).strict()).max(6),
200
+ }).strict()).max(6),
201
+ }).strict();
202
+
203
+ const pptxInspectionSummary = z.object({
204
+ slideCount: z.number().int().nonnegative(),
205
+ masterCount: z.number().int().nonnegative(),
206
+ slideSize: z.object({
207
+ cx: z.number().int().nonnegative(),
208
+ cy: z.number().int().nonnegative(),
209
+ }).strict().nullable(),
210
+ openingSlides: z.array(z.object({
211
+ slideNumber: z.number().int().positive(),
212
+ textPreview: z.string(),
213
+ }).strict()).max(6),
214
+ }).strict();
215
+
182
216
  function docxObservationOutput(tool) {
183
217
  return z.object({
184
218
  tool: z.literal(tool),
@@ -519,7 +553,7 @@ const tools = [
519
553
  name: 'xlsx_inspect',
520
554
  description: 'Inspect a current XLSX workbook or legacy XLS workbook, including workbook structure, exported values, formulas, styles, merged ranges, and published legacy-format conversion evidence. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
521
555
  inputSchema: inputContract('xlsx_inspect'),
522
- outputSchema: largeResultOutput('xlsx_inspect'),
556
+ outputSchema: largeInspectionOutput('xlsx_inspect', xlsxInspectionSummary),
523
557
  annotations: { readOnlyHint: true, idempotentHint: true },
524
558
  handler: xlsxInspect,
525
559
  },
@@ -544,7 +578,7 @@ const tools = [
544
578
  name: 'pptx_inspect',
545
579
  description: 'Inspect a PPTX file, including slides, masters, layouts, shapes, transforms, paragraphs, runs, and placeholders. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
546
580
  inputSchema: inputContract('pptx_inspect'),
547
- outputSchema: largeResultOutput('pptx_inspect'),
581
+ outputSchema: largeInspectionOutput('pptx_inspect', pptxInspectionSummary),
548
582
  annotations: { readOnlyHint: true, idempotentHint: true },
549
583
  handler: pptxInspect,
550
584
  },
@@ -611,7 +645,9 @@ function buildServer() {
611
645
  ...(tool.annotations ? { annotations: tool.annotations } : {}),
612
646
  },
613
647
  async args => {
614
- const payload = await tool.handler(args);
648
+ const payload = typeof args.output === 'string'
649
+ ? await withOutputWriteLock(args.output, () => tool.handler(args))
650
+ : await tool.handler(args);
615
651
  return createToolResult(payload, { isError: payload?.summary?.pass === false });
616
652
  },
617
653
  );
@@ -1146,7 +1182,14 @@ function nativeRenderBackend(sourceFormat) {
1146
1182
  async function xlsxInspect(args) {
1147
1183
  const input = path.resolve(requireString(args.input, 'input'));
1148
1184
  const result = await runJsonCandidateChain(xlsxCandidates, ['inspect', input, '--json']);
1149
- return deliverLargeJsonResult({ tool: 'xlsx_inspect', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1185
+ return deliverLargeJsonResult({
1186
+ tool: 'xlsx_inspect',
1187
+ args,
1188
+ runtime: commandRuntime(result),
1189
+ payload: result.json,
1190
+ sourcePaths: [input],
1191
+ summary: compactXlsxInspection(result.json),
1192
+ });
1150
1193
  }
1151
1194
 
1152
1195
  async function xlsxExportJson(args) {
@@ -1168,7 +1211,14 @@ async function xlsxValidate(args) {
1168
1211
  async function pptxInspect(args) {
1169
1212
  const input = path.resolve(requireString(args.input, 'input'));
1170
1213
  const result = await runJsonCandidateChain(pptxCandidates, ['inspect', input, '--json']);
1171
- return deliverLargeJsonResult({ tool: 'pptx_inspect', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1214
+ return deliverLargeJsonResult({
1215
+ tool: 'pptx_inspect',
1216
+ args,
1217
+ runtime: commandRuntime(result),
1218
+ payload: result.json,
1219
+ sourcePaths: [input],
1220
+ summary: compactPptxInspection(result.json),
1221
+ });
1172
1222
  }
1173
1223
 
1174
1224
  async function pptxExportJson(args) {
@@ -1183,6 +1233,58 @@ async function pptxValidate(args) {
1183
1233
  return deliverLargeJsonResult({ tool: 'pptx_validate', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1184
1234
  }
1185
1235
 
1236
+ function compactPreview(value, maximum) {
1237
+ return typeof value === 'string'
1238
+ ? value.trim().replace(/\s+/gu, ' ').slice(0, maximum)
1239
+ : '';
1240
+ }
1241
+
1242
+ function compactXlsxInspection(payload) {
1243
+ const workbook = payload?.workbook ?? {};
1244
+ const allSheets = Array.isArray(workbook.sheets) ? workbook.sheets : [];
1245
+ return {
1246
+ sheetCount: Number.isInteger(workbook.sheetCount) ? workbook.sheetCount : allSheets.length,
1247
+ sheets: allSheets.slice(0, 6).map(sheet => ({
1248
+ name: compactPreview(sheet?.name, 120),
1249
+ rowCount: Number.isInteger(sheet?.rowCount) ? sheet.rowCount : 0,
1250
+ columnCount: Number.isInteger(sheet?.columnCount) ? sheet.columnCount : 0,
1251
+ usedRange: typeof sheet?.usedRange === 'string' ? compactPreview(sheet.usedRange, 120) : null,
1252
+ mergedRangeCount: Array.isArray(sheet?.mergedRanges) ? sheet.mergedRanges.length : 0,
1253
+ formulaCellCount: Number.isInteger(sheet?.formulaCellCount) ? sheet.formulaCellCount : 0,
1254
+ openingText: (Array.isArray(sheet?.textCells) ? sheet.textCells : [])
1255
+ .filter(cell => typeof cell?.text === 'string' && cell.text.trim() !== '')
1256
+ .slice(0, 6)
1257
+ .map(cell => ({
1258
+ reference: compactPreview(cell.reference, 40),
1259
+ textPreview: compactPreview(cell.text, 160),
1260
+ })),
1261
+ })),
1262
+ };
1263
+ }
1264
+
1265
+ function compactPptxInspection(payload) {
1266
+ const slides = Array.isArray(payload?.slides) ? payload.slides : [];
1267
+ return {
1268
+ slideCount: Number.isInteger(payload?.slideCount) ? payload.slideCount : slides.length,
1269
+ masterCount: Array.isArray(payload?.masters) ? payload.masters.length : 0,
1270
+ slideSize: Number.isInteger(payload?.slideSize?.cx) && Number.isInteger(payload?.slideSize?.cy)
1271
+ ? { cx: payload.slideSize.cx, cy: payload.slideSize.cy }
1272
+ : null,
1273
+ openingSlides: slides.slice(0, 6).map((slide, index) => ({
1274
+ slideNumber: Number.isInteger(slide?.slideNumber) && slide.slideNumber > 0
1275
+ ? slide.slideNumber
1276
+ : index + 1,
1277
+ textPreview: compactPreview(
1278
+ (Array.isArray(slide?.shapes) ? slide.shapes : [])
1279
+ .map(shape => shape?.text)
1280
+ .filter(text => typeof text === 'string' && text.trim() !== '')
1281
+ .join(' '),
1282
+ 240,
1283
+ ),
1284
+ })),
1285
+ };
1286
+ }
1287
+
1186
1288
  async function requireReturnedArtifact(returned, expectedPath, label) {
1187
1289
  if (!returned || path.resolve(returned.path || '') !== expectedPath) {
1188
1290
  throw new Error(`${label} artifact is not bound to the accepted provider call`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "_shared/tool-runtime.mjs",
20
20
  "_shared/large-json-result.mjs",
21
+ "_shared/output-write-lock.mjs",
21
22
  "office/index.mjs",
22
23
  "office/README.md",
23
24
  "office/contracts/tiwater-office-provider-contract-manifest-v1.json",