@tiwater/office-mcp 0.21.22 → 0.21.28

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,6 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "tiwater.office-mcp-input/docx_fill_table_from_tables",
4
+ "description": "Project source-table rows into one target template table atomically. Use this instead of manually rebuilding rows when target cells derive from source tables. Target formatting is reused, and native vertical-merge owners are preserved within each source range.",
4
5
  "type": "object",
5
6
  "properties": {
6
7
  "input": { "type": "string", "minLength": 1, "description": "Current target DOCX.", "x-tiwater-file-role": "read" },
@@ -96,7 +97,7 @@
96
97
  },
97
98
  "recordColumn": {
98
99
  "type": "object",
99
- "description": "Source grid-column address whose native vertical-merge owners define consecutive logical records. An unmerged cell defines one record.",
100
+ "description": "Source grid-column address that defines output-row records. Choose an unmerged column when every physical source row must remain an output row; vertical merges in other mapped columns are still preserved. Choose a merged column only when every mapping into one target cell has one equal scalar value throughout that merge group.",
100
101
  "properties": {
101
102
  "part": { "type": "string", "minLength": 1, "pattern": "^\\/" },
102
103
  "path": { "type": "string", "pattern": "^\\/[A-Za-z_][A-Za-z0-9_.-]*:[A-Za-z_][A-Za-z0-9_.-]*\\[[1-9][0-9]*\\](?:\\/[A-Za-z_][A-Za-z0-9_.-]*:[A-Za-z_][A-Za-z0-9_.-]*\\[[1-9][0-9]*\\])*$" }
@@ -44,13 +44,13 @@
44
44
  "sourceSelections": {
45
45
  "minItems": 1,
46
46
  "type": "array",
47
- "description": "Ordered native source paragraphs, runs, or text nodes. Their selected inline content becomes the target content without Agent transcription.",
47
+ "description": "Ordered native source cells, paragraphs, runs, or text nodes. Consecutive run or text selections from one source paragraph form one target paragraph; a range retains the native runs it crosses. A source paragraph boundary remains a target paragraph boundary. Selected content becomes the target content without Agent transcription.",
48
48
  "items": {
49
49
  "type": "object",
50
50
  "properties": {
51
51
  "address": {
52
52
  "type": "object",
53
- "description": "Native source object address returned by docx_read_object or docx_read_table. Selecting a paragraph copies that paragraph's inline content; selecting a cell copies its direct paragraphs.",
53
+ "description": "Native source object address returned by docx_read_object or docx_read_table. Selecting a paragraph copies that paragraph's inline content; selecting a cell copies its direct paragraphs; selecting a run or text copies native inline content and formatting.",
54
54
  "properties": {
55
55
  "part": {
56
56
  "type": "string",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "range": {
72
72
  "type": "object",
73
- "description": "Optional Unicode-scalar substring of a selected run or text node. Omit it to copy the complete selected object.",
73
+ "description": "Optional Unicode-scalar substring of the selected object's observed text. For a cell, offsets use its docx_read_table text with one newline between direct paragraphs. The selected native runs and paragraph boundaries are retained. Omit it to copy the complete selected object.",
74
74
  "properties": {
75
75
  "start": {
76
76
  "type": "integer",
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "tiwater.office-mcp-input/docx_set_paragraph_pagination",
4
+ "type": "object",
5
+ "properties": {
6
+ "input": { "type": "string", "minLength": 1, "x-tiwater-file-role": "read" },
7
+ "changes": {
8
+ "type": "array",
9
+ "minItems": 1,
10
+ "items": {
11
+ "type": "object",
12
+ "properties": {
13
+ "paragraph": {
14
+ "type": "object",
15
+ "properties": {
16
+ "part": { "type": "string", "minLength": 1, "pattern": "^\\/" },
17
+ "path": { "type": "string", "pattern": "^\\/[A-Za-z_][A-Za-z0-9_.-]*:[A-Za-z_][A-Za-z0-9_.-]*\\[[1-9][0-9]*\\](?:\\/[A-Za-z_][A-Za-z0-9_.-]*:[A-Za-z_][A-Za-z0-9_.-]*\\[[1-9][0-9]*\\])*$" }
18
+ },
19
+ "required": ["part", "path"],
20
+ "additionalProperties": false
21
+ },
22
+ "keepWithNext": { "type": "boolean", "description": "Keep this paragraph on the same page as the immediately following paragraph or table. This does not guarantee that a table header remains with its first body row." },
23
+ "keepLinesTogether": { "type": "boolean", "description": "Keep all lines of this paragraph on one page." },
24
+ "pageBreakBefore": { "type": "boolean", "description": "Start this paragraph on a new page." },
25
+ "preventWidowOrphanLines": { "type": "boolean", "description": "Prevent a single first or last line from being isolated across pages." }
26
+ },
27
+ "required": ["paragraph"],
28
+ "additionalProperties": false
29
+ }
30
+ },
31
+ "output": { "type": "string", "minLength": 1, "description": "Output DOCX path; may equal input for one atomic in-place update.", "x-tiwater-file-role": "write" },
32
+ "receiptOutput": { "type": "string", "minLength": 1, "x-tiwater-file-role": "write", "x-tiwater-file-effect": false }
33
+ },
34
+ "required": ["input", "changes", "output", "receiptOutput"],
35
+ "additionalProperties": false
36
+ }
@@ -94,7 +94,10 @@
94
94
  "description": "One or more contiguous request-local column IDs occupied by this cell.",
95
95
  "items": { "type": "string", "minLength": 1, "maxLength": 80 }
96
96
  },
97
- "text": { "type": "string" },
97
+ "text": {
98
+ "type": "string",
99
+ "description": "Final plain text for this cell. If the current cell at the same row and grid span already has exactly this text, its native paragraphs and run formatting are retained while row and merge properties are updated."
100
+ },
98
101
  "rowSpan": {
99
102
  "type": "integer",
100
103
  "minimum": 1,
@@ -105,6 +108,10 @@
105
108
  "required": ["columns", "text"],
106
109
  "additionalProperties": false
107
110
  }
111
+ },
112
+ "cantSplit": {
113
+ "type": "boolean",
114
+ "description": "Whether Word may split this physical row across pages. Omit to preserve the prototype row property."
108
115
  }
109
116
  },
110
117
  "required": ["prototypeRow", "cells"],
@@ -19,12 +19,6 @@
19
19
  "description": "Optional new JSON artifact path for retaining the complete table index. May be combined with returnContent.",
20
20
  "x-tiwater-file-role": "write"
21
21
  },
22
- "limit": {
23
- "type": "integer",
24
- "minimum": 1,
25
- "maximum": 1000,
26
- "description": "Maximum number of table summaries requested in this response page. The provider may return fewer to keep the machine response bounded."
27
- },
28
22
  "offset": {
29
23
  "type": "integer",
30
24
  "minimum": 0,
@@ -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.22"
5
+ "version": "0.21.28"
6
6
  },
7
7
  "tools": [
8
8
  {
@@ -75,11 +75,11 @@
75
75
  "name": "docx_fill_table_from_tables",
76
76
  "providerContract": {
77
77
  "source": "packages/docx-cli/contracts/mcp-input/docx_fill_table_from_tables.schema.json",
78
- "sha256": "f4778f68707c1bc505bd1b38c26508b9ff80d2bc1a361f1630ecb70dbc6a542e"
78
+ "sha256": "4e80fff1c08fd16f8a5b4c638cf6469aa51e3a499743b44f3bf900ae7194ea9b"
79
79
  },
80
80
  "inputContract": {
81
81
  "path": "office/contracts/docx_fill_table_from_tables.schema.json",
82
- "sha256": "f4778f68707c1bc505bd1b38c26508b9ff80d2bc1a361f1630ecb70dbc6a542e"
82
+ "sha256": "4e80fff1c08fd16f8a5b4c638cf6469aa51e3a499743b44f3bf900ae7194ea9b"
83
83
  }
84
84
  },
85
85
  {
@@ -174,11 +174,11 @@
174
174
  "name": "docx_replace_content_from_source",
175
175
  "providerContract": {
176
176
  "source": "packages/docx-cli/contracts/mcp-input/docx_replace_content_from_source.schema.json",
177
- "sha256": "d6ce53f684ea0991508cab930e101c910ac05e9725cc25d5312806070c20d320"
177
+ "sha256": "3ce7730317f1b9429f5da38b0eaecb53664a056545904a7f3904cdf148b94ad1"
178
178
  },
179
179
  "inputContract": {
180
180
  "path": "office/contracts/docx_replace_content_from_source.schema.json",
181
- "sha256": "d6ce53f684ea0991508cab930e101c910ac05e9725cc25d5312806070c20d320"
181
+ "sha256": "3ce7730317f1b9429f5da38b0eaecb53664a056545904a7f3904cdf148b94ad1"
182
182
  }
183
183
  },
184
184
  {
@@ -192,15 +192,26 @@
192
192
  "sha256": "25f4d4e7c69fa5a2c4a82621aa58ea8eda21245636f84f2243ca4e9092616b68"
193
193
  }
194
194
  },
195
+ {
196
+ "name": "docx_set_paragraph_pagination",
197
+ "providerContract": {
198
+ "source": "packages/docx-cli/contracts/mcp-input/docx_set_paragraph_pagination.schema.json",
199
+ "sha256": "ccb90bb5b811cfc82985050094637b1afe4c9daf51d7243c0d81b2d63d875825"
200
+ },
201
+ "inputContract": {
202
+ "path": "office/contracts/docx_set_paragraph_pagination.schema.json",
203
+ "sha256": "ccb90bb5b811cfc82985050094637b1afe4c9daf51d7243c0d81b2d63d875825"
204
+ }
205
+ },
195
206
  {
196
207
  "name": "docx_set_table_body",
197
208
  "providerContract": {
198
209
  "source": "packages/docx-cli/contracts/mcp-input/docx_set_table_body.schema.json",
199
- "sha256": "c1d462210f14b6a2761d4056e0a3001925987e53e38075a5c44572d0b37d289b"
210
+ "sha256": "adb0898a253a60fc80e3e811fdd6d19ac01ae76abe16b077e8af1f0960b640af"
200
211
  },
201
212
  "inputContract": {
202
213
  "path": "office/contracts/docx_set_table_body.schema.json",
203
- "sha256": "c1d462210f14b6a2761d4056e0a3001925987e53e38075a5c44572d0b37d289b"
214
+ "sha256": "adb0898a253a60fc80e3e811fdd6d19ac01ae76abe16b077e8af1f0960b640af"
204
215
  }
205
216
  },
206
217
  {
@@ -240,11 +251,11 @@
240
251
  "name": "docx_table_index",
241
252
  "providerContract": {
242
253
  "source": "packages/docx-cli/contracts/mcp-input/docx_table_index.schema.json",
243
- "sha256": "a5f43ef56c14dfa846d4823b244cadac141f45fe1b716782df421c270f6efc5e"
254
+ "sha256": "737052f653fe0d222bfc2b808e0fbc298376ab88cf052352888ff02e5b71ed52"
244
255
  },
245
256
  "inputContract": {
246
257
  "path": "office/contracts/docx_table_index.schema.json",
247
- "sha256": "a5f43ef56c14dfa846d4823b244cadac141f45fe1b716782df421c270f6efc5e"
258
+ "sha256": "737052f653fe0d222bfc2b808e0fbc298376ab88cf052352888ff02e5b71ed52"
248
259
  }
249
260
  },
250
261
  {
@@ -445,6 +456,17 @@
445
456
  "sha256": "9ae4a846a8ca5b9059dcfd2a589e9872a2ab8d4ba3a118a513f3e05462f923d0"
446
457
  }
447
458
  },
459
+ {
460
+ "name": "xlsx_read_range",
461
+ "providerContract": {
462
+ "source": "packages/xlsx-cli/contracts/mcp-input/xlsx_read_range.schema.json",
463
+ "sha256": "04b0ce1b912047f774fbbbb6ba7cf55e5e53a9b74d6f027d9c21b4d6d7397225"
464
+ },
465
+ "inputContract": {
466
+ "path": "office/contracts/xlsx_read_range.schema.json",
467
+ "sha256": "04b0ce1b912047f774fbbbb6ba7cf55e5e53a9b74d6f027d9c21b4d6d7397225"
468
+ }
469
+ },
448
470
  {
449
471
  "name": "xlsx_set_cell_number_format",
450
472
  "providerContract": {
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "tiwater.office-mcp-input/xlsx_read_range/v1",
4
+ "type": "object",
5
+ "properties": {
6
+ "input": {
7
+ "type": "string",
8
+ "minLength": 1,
9
+ "description": "Current XLSX workbook. Convert legacy XLS before selecting Open XML cells.",
10
+ "x-tiwater-file-role": "read"
11
+ },
12
+ "sheet": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "description": "Exact current worksheet name returned by workbook observation."
16
+ },
17
+ "range": {
18
+ "type": "string",
19
+ "pattern": "^[A-Za-z]{1,3}[1-9][0-9]*(?::[A-Za-z]{1,3}[1-9][0-9]*)?$",
20
+ "description": "Explicit native A1 cell or rectangular range. A single cell is a one-cell range."
21
+ },
22
+ "offset": {
23
+ "type": "integer",
24
+ "minimum": 0,
25
+ "maximum": 9007199254740991,
26
+ "description": "Zero-based cell offset in row-major order within the selected range. Continue only when another selected cell is needed."
27
+ },
28
+ "limit": {
29
+ "type": "integer",
30
+ "minimum": 1,
31
+ "maximum": 256,
32
+ "description": "Maximum selected cells in this bounded page."
33
+ },
34
+ "returnContent": {
35
+ "type": "boolean",
36
+ "description": "Return this selected technical page directly when it fits the response limit. May be combined with output."
37
+ },
38
+ "output": {
39
+ "type": "string",
40
+ "minLength": 1,
41
+ "description": "Optional immutable JSON artifact path for this selected technical page. May be combined with returnContent.",
42
+ "x-tiwater-file-role": "write"
43
+ }
44
+ },
45
+ "required": ["input", "sheet", "range", "limit", "returnContent"],
46
+ "additionalProperties": false
47
+ }
@@ -0,0 +1,11 @@
1
+ export function compactDocxObjectIdentity(object) {
2
+ return {
3
+ address: object.address,
4
+ parentAddress: object.parentAddress ?? null,
5
+ kind: object.kind,
6
+ textPreview: object.textPreview ?? null,
7
+ gridSpan: object.gridSpan ?? null,
8
+ verticalMerge: object.verticalMerge ?? null,
9
+ verticalTextAlignment: object.verticalTextAlignment ?? null,
10
+ };
11
+ }
package/office/index.mjs CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  writeJsonArtifact,
24
24
  } from '../_shared/large-json-result.mjs';
25
25
  import { withOutputWriteLock } from '../_shared/output-write-lock.mjs';
26
+ import { compactDocxObjectIdentity } from './docx-object-identity.mjs';
26
27
 
27
28
  const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
28
29
  const inputContractManifest = JSON.parse(await readFile(
@@ -201,6 +202,67 @@ const xlsxInspectionSummary = z.object({
201
202
  }).strict()).max(6),
202
203
  }).strict();
203
204
 
205
+ const xlsxRangePageSummary = z.object({
206
+ schema: z.literal('tiwater.xlsx-range-page-receipt/v1'),
207
+ totalCellCount: z.number().int().nonnegative(),
208
+ returnedCellCount: z.number().int().nonnegative(),
209
+ remaining: z.number().int().nonnegative(),
210
+ nextOffset: z.number().int().nonnegative().nullable(),
211
+ }).strict();
212
+
213
+ const xlsxRangePage = z.object({
214
+ schema: z.literal('tiwater.xlsx-range-page/v1'),
215
+ toolVersion: z.string(),
216
+ file: z.string(),
217
+ inputSha256: z.string().regex(/^[a-f0-9]{64}$/),
218
+ sheet: z.string(),
219
+ range: z.string(),
220
+ receipt: xlsxRangePageSummary,
221
+ cells: z.array(z.object({
222
+ reference: z.string(),
223
+ row: z.number().int().positive(),
224
+ column: z.number().int().positive(),
225
+ physical: z.boolean(),
226
+ rawValue: z.string().nullable(),
227
+ formattedValue: z.string().nullable(),
228
+ valueType: z.string().nullable(),
229
+ normalizedValue: z.object({ kind: z.string(), iso8601: z.string().nullable() }).strict().nullable(),
230
+ formula: z.object({
231
+ text: z.string(),
232
+ type: z.string().nullable(),
233
+ sharedIndex: z.number().int().nonnegative().nullable(),
234
+ reference: z.string().nullable(),
235
+ }).strict().nullable(),
236
+ style: z.object({
237
+ styleIndex: z.number().int().nonnegative(),
238
+ numberFormatId: z.number().int().nonnegative(),
239
+ numberFormatCode: z.string().nullable(),
240
+ fontId: z.number().int().nonnegative(),
241
+ fillId: z.number().int().nonnegative(),
242
+ borderId: z.number().int().nonnegative(),
243
+ horizontalAlignment: z.string().nullable(),
244
+ verticalAlignment: z.string().nullable(),
245
+ wrapText: z.boolean(),
246
+ bold: z.boolean(),
247
+ }).strict().nullable(),
248
+ richTextRuns: z.array(z.object({
249
+ text: z.string(),
250
+ fontName: z.string().nullable(),
251
+ color: z.string().nullable(),
252
+ underline: z.string().nullable(),
253
+ bold: z.boolean(),
254
+ italic: z.boolean(),
255
+ }).strict()).nullable(),
256
+ mergedRange: z.string().nullable(),
257
+ mergeOwner: z.string().nullable(),
258
+ }).strict()).max(256),
259
+ }).strict();
260
+
261
+ const xlsxRangeReadOutput = largeResultOutput('xlsx_read_range').extend({
262
+ summary: xlsxRangePageSummary,
263
+ content: xlsxRangePage.optional(),
264
+ }).strict();
265
+
204
266
  const pptxInspectionSummary = z.object({
205
267
  slideCount: z.number().int().nonnegative(),
206
268
  masterCount: z.number().int().nonnegative(),
@@ -237,7 +299,7 @@ function docxInspectionOutput(tool) {
237
299
  address: docxAddress,
238
300
  textPreview: z.string().min(1).max(240),
239
301
  }).strict()).max(6),
240
- }).strict().optional(),
302
+ }).strict(),
241
303
  }).strict();
242
304
  }
243
305
 
@@ -248,6 +310,7 @@ const docxObjectIdentity = z.object({
248
310
  textPreview: z.string().nullable(),
249
311
  gridSpan: z.number().int().positive().nullable(),
250
312
  verticalMerge: z.string().nullable(),
313
+ verticalTextAlignment: z.enum(['baseline', 'superscript', 'subscript']).nullable(),
251
314
  }).strict();
252
315
 
253
316
  const docxNestedObjectIdentity = docxObjectIdentity.pick({
@@ -259,6 +322,7 @@ const docxNestedObjectIdentity = docxObjectIdentity.pick({
259
322
  verticalMerge: z.string().optional(),
260
323
  verticalMergeOwner: docxAddress.optional(),
261
324
  logicalText: z.string().optional(),
325
+ verticalTextAlignment: z.enum(['baseline', 'superscript', 'subscript']).optional(),
262
326
  }).strict();
263
327
  const docxObservationNode = z.lazy(() => z.object({
264
328
  object: docxNestedObjectIdentity,
@@ -292,21 +356,11 @@ const docxTableIndexOutput = docxObservationOutput('docx_table_index').extend({
292
356
  }).strict(),
293
357
  tables: z.array(z.object({
294
358
  address: docxAddress,
295
- parentAddress: docxAddress.nullable(),
296
359
  rowCount: z.number().int().nonnegative(),
297
360
  columnCount: z.number().int().nonnegative(),
298
361
  textPreview: z.string(),
299
- textLength: z.number().int().nonnegative(),
300
- precedingParagraph: z.object({
301
- address: docxAddress,
302
- textPreview: z.string(),
303
- textLength: z.number().int().nonnegative(),
304
- }).strict().nullable(),
305
- followingParagraph: z.object({
306
- address: docxAddress,
307
- textPreview: z.string(),
308
- textLength: z.number().int().nonnegative(),
309
- }).strict().nullable(),
362
+ precedingText: z.string().nullable(),
363
+ followingText: z.string().nullable(),
310
364
  }).strict()).optional(),
311
365
  }).strict();
312
366
 
@@ -370,7 +424,7 @@ const docxReadObjectOutput = docxObservationOutput('docx_read_object').extend({
370
424
  const tools = [
371
425
  {
372
426
  name: 'docx_inspect',
373
- description: 'Inspect one current DOCX. Set returnContent true to return the compact identity summary. Provide output to retain the complete machine observation and return its artifact receipt. These channels are independent and may be used together. At least one channel is required. Use list and read operations to traverse selected document objects in native structure order.',
427
+ description: 'Inspect one current DOCX for identity and package overview. The response always includes a bounded identity summary. Set returnContent true when that summary is the requested direct result. Provide output to retain the complete machine observation and return its artifact receipt. These channels are independent and may be used together. At least one channel is required. Use list and read operations to traverse selected document objects in native structure order. This overview is not a complete final-document readback; use docx_export_json when a downstream consumer requires the complete body projection.',
374
428
  inputSchema: inputContract('docx_inspect'),
375
429
  outputSchema: docxInspectionOutput('docx_inspect'),
376
430
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -386,7 +440,7 @@ const tools = [
386
440
  },
387
441
  {
388
442
  name: 'docx_table_index',
389
- description: 'Locate tables in one current DOCX without returning full cell content or deciding table semantics. Set returnContent true to return a bounded page of addresses, shapes, short previews, and nearest non-empty paragraphs. Provide output to store the complete index and return its artifact receipt. These channels are independent and may be used together; at least one is required. Continue from receipt.nextOffset only when an unreturned table is needed for the current decision, then read one selected native address.',
443
+ description: 'Locate tables in one current DOCX without returning full cell content or deciding table semantics. Set returnContent true to return as many compact native addresses, shapes, and short text clues as fit the bounded response; the provider chooses page size. Provide output to store the complete index and return its artifact receipt. These channels are independent and may be used together; at least one is required. Continue from receipt.nextOffset only when an unreturned table is needed for the current decision, then pass one returned address unchanged to a narrow table or object read.',
390
444
  inputSchema: inputContract('docx_table_index'),
391
445
  outputSchema: docxTableIndexOutput,
392
446
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -394,7 +448,7 @@ const tools = [
394
448
  },
395
449
  {
396
450
  name: 'docx_read_object',
397
- description: 'Read explicitly selected rows, cells, or paragraphs from one native DOCX. Set returnContent true to return compact requested descendants; if receipt.narrowingRequired is true, request fewer addresses or descendant kinds. Provide output to store the complete selected observation and return its artifact receipt. These channels are independent and may be used together; at least one is required. A selected cell exposes its vertical-merge owner and logical text, so a continue cell keeps its physical identity while resolving the restart cell value. Use docx_read_table for a table range.',
451
+ description: 'Read explicitly selected rows, cells, or paragraphs from one native DOCX. Set returnContent true to return compact requested descendants; if receipt.narrowingRequired is true, request fewer addresses or descendant kinds. Provide output to store the complete selected observation and return its artifact receipt. These channels are independent and may be used together; at least one is required. A selected cell exposes its vertical-merge owner and logical text, so a continue cell keeps its physical identity while resolving the restart cell value. Run and text descendants expose their native verticalTextAlignment when it is baseline, superscript, or subscript. Use docx_read_table for a table range.',
398
452
  inputSchema: inputContract('docx_read_object'),
399
453
  outputSchema: docxReadObjectOutput,
400
454
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -402,7 +456,7 @@ const tools = [
402
456
  },
403
457
  {
404
458
  name: 'docx_read_table',
405
- description: 'Read an explicit row range from exactly one table selected by native OpenXML address; it never builds another whole-table data object. Set returnContent true to return a byte-bounded compact row page. Provide output to store full paragraph and text-node detail for the selected row page and return its artifact receipt. These channels are independent and may be used together; at least one is required. Request only rows needed for the current decision; receipt.remaining is navigation information, not an obligation to read unused rows, and blank template rows need not be paged through. receipt.nextOffset is present only when another row page exists. Each returned row and cell keeps its reusable native address, zero-based logical gridColumnStart, gridSpan, vertical-merge owner, physical text, and logical text. Match columns across rows by gridColumnStart; a tc[n] path or array position is only that row\'s physical cell ordinal and is not a column identity when gridSpan or gridBefore is present. In a vertical merge, restart begins one logical cell and a continue cell is not an independent row value: logicalText resolves the restart cell value while text remains the physical cell value. Use docx_read_object when one exact object needs a narrower descendant view. The provider reports physical structure only; the Agent decides template and business meaning.',
459
+ description: 'Read an explicit row range from exactly one table selected by native OpenXML address; it never builds another whole-table data object. Set returnContent true to return a byte-bounded compact row page. Provide output to store full paragraph and text-node detail, including native verticalTextAlignment, for the selected row page and return its artifact receipt. These channels are independent and may be used together; at least one is required. Request only rows needed for the current decision; receipt.remaining is navigation information, not an obligation to read unused rows, and blank template rows need not be paged through. receipt.nextOffset is present only when another row page exists. Each returned row and cell keeps its reusable native address, zero-based logical gridColumnStart, gridSpan, vertical-merge owner, physical text, and logical text. Match columns across rows by gridColumnStart; a tc[n] path or array position is only that row\'s physical cell ordinal and is not a column identity when gridSpan or gridBefore is present. In a vertical merge, restart begins one logical cell and a continue cell is not an independent row value: logicalText resolves the restart cell value while text remains the physical cell value. Use docx_read_object when one exact object needs a narrower descendant view. The provider reports physical structure only; the Agent decides template and business meaning.',
406
460
  inputSchema: inputContract('docx_read_table'),
407
461
  outputSchema: docxTableReadOutput,
408
462
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -410,28 +464,35 @@ const tools = [
410
464
  },
411
465
  {
412
466
  name: 'docx_replace_content_from_source',
413
- description: 'Replace existing target paragraph or table-cell content from explicitly selected native source paragraphs, runs, text nodes, or exact text ranges while retaining target container formatting and table structure. Use it after docx_fill_table_from_tables when the target needs selected source child content instead of the whole source cell; supply returned target-cell addresses and observed source descendant addresses, never retype source-owned text. It does not copy source rows, cells, spans, or merges.',
467
+ description: 'Replace existing target paragraph or table-cell content from explicitly selected native source cells, paragraphs, runs, text nodes, or exact text ranges while retaining target container formatting and table structure. For a source cell already returned by docx_read_table, select a Unicode-scalar range directly against its returned text; the provider retains every crossed native run, including superscript and subscript, without another descendant read. Consecutive run or text selections from one source paragraph form one target paragraph, and source paragraph boundaries remain paragraph boundaries. Use it after docx_fill_table_from_tables when the target needs selected source content instead of the whole source cell; pass returned addresses unchanged and never retype source-owned text. It does not copy source rows, cells, spans, or merges.',
414
468
  inputSchema: inputContract('docx_replace_content_from_source'),
415
469
  outputSchema: fixedEditOutput('docx_replace_content_from_source'),
416
470
  handler: args => fixedEdit('docx_replace_content_from_source', args),
417
471
  },
418
472
  {
419
473
  name: 'docx_set_text',
420
- description: 'Replace the whole text content of paragraph or cell objects observed from this exact input DOCX while retaining target formatting, bookmarks, spans, and vertical merges. For a vertically merged logical cell, write its visible text to the restart cell rather than a continue cell. Tabs and line breaks remain native document text controls; targets containing non-text objects are rejected. This sets already-derived text; it does not insert objects, change table structure, copy source formatting, or decide business wording.',
474
+ description: 'Replace the whole text content of paragraph or cell objects observed from this exact input DOCX while retaining target formatting, bookmarks, spans, and vertical merges. For a vertically merged logical cell, write its visible text to the restart cell rather than a continue cell. Tabs and line breaks remain native document text controls; targets containing non-text objects are rejected. Use this only for newly derived text. Content copied or selected from a source DOCX uses docx_replace_content_from_source so native runs such as superscript and subscript are retained. This does not insert objects, change table structure, copy source formatting, or decide business wording.',
421
475
  inputSchema: inputContract('docx_set_text'),
422
476
  outputSchema: fixedEditOutput('docx_set_text'),
423
477
  handler: args => fixedEdit('docx_set_text', args),
424
478
  },
479
+ {
480
+ name: 'docx_set_paragraph_pagination',
481
+ description: 'Set native pagination properties on explicitly selected current DOCX paragraphs. Each change sets at least one pagination property. keepWithNext keeps a paragraph with the immediately following paragraph or table but does not guarantee that a table header remains with its first body row. keepLinesTogether keeps one paragraph on one page; pageBreakBefore starts it on a new page; preventWidowOrphanLines controls isolated first or last lines. Omitted properties remain unchanged. The caller chooses paragraphs from current native addresses; the provider does not decide document layout or business meaning.',
482
+ inputSchema: inputContract('docx_set_paragraph_pagination'),
483
+ outputSchema: fixedEditOutput('docx_set_paragraph_pagination'),
484
+ handler: args => fixedEdit('docx_set_paragraph_pagination', args),
485
+ },
425
486
  {
426
487
  name: 'docx_set_table_body',
427
- description: 'Atomically replace one exact current target-table row range while retaining the table, target styles, grid widths, and all rows and surrounding content outside the range. When retaining leading rows reported with repeatHeader=true, existingRows starts after all of them and never at a verticalMerge=continue row. Name every target grid column in native order and choose one current row inside existingRows as the style prototype for each final row. Horizontal spans use contiguous column IDs. Set rowSpan on one logical cell to occupy multiple rows and omit those columns from the covered rows; the provider writes native vertical merge cells. Every other grid column remains explicit. Every explicit cell includes an already-derived value; source-owned content is not retyped here. An empty final row array removes the range when another table row remains. The provider commits once and returns structural readback. It does not read source tables, map source to target, derive text, choose business columns, identify headers, or accept non-text cell content; use docx_fill_table_from_tables and docx_replace_content_from_source for source-owned table content.',
488
+ description: 'Atomically replace one exact current target-table row range while retaining the table, target styles, grid widths, and all rows and surrounding content outside the range. When retaining leading rows reported with repeatHeader=true, existingRows starts after all of them and never at a verticalMerge=continue row. Name every target grid column in native order and choose one current row inside existingRows as the style prototype for each final row. Horizontal spans use contiguous column IDs. Set rowSpan on one logical cell to occupy multiple rows and omit those columns from the covered rows; the provider writes native vertical merge cells. Set cantSplit on a final physical row when it must remain whole across page boundaries; omit it to preserve the prototype row property. Every other grid column remains explicit. Every explicit cell includes an already-derived value; source-owned content is not retyped here. An empty final row array removes the range when another table row remains. The provider commits once and returns structural readback. It does not read source tables, map source to target, derive text, choose business columns, identify headers, or accept non-text cell content; use docx_fill_table_from_tables and docx_replace_content_from_source for source-owned table content.',
428
489
  inputSchema: inputContract('docx_set_table_body'),
429
490
  outputSchema: fixedEditOutput('docx_set_table_body'),
430
491
  handler: args => fixedEdit('docx_set_table_body', args),
431
492
  },
432
493
  {
433
494
  name: 'docx_fill_table_from_tables',
434
- description: 'Fill one current target-table body from one or more explicitly ordered current source-table row ranges. For each source, select the grid column whose native vertical merges define logical records and map its grid columns onto every target prototype cell. The provider concatenates source records in declared order, preserves horizontal spans, rebuilds vertical merges only within each source range, validates the complete target grid, commits once, and returns structural readback. It does not discover source tables, choose source or target business meaning, filter records, translate or rewrite text, or apply business-specific rules. A single source uses the same sources array with one item. When only selected source paragraphs belong in the target, use its returned target-cell addresses with docx_replace_content_from_source before readback.',
495
+ description: 'Fill one current target-table body from one or more explicitly ordered current source-table row ranges. For each source, select an unmerged record column when every physical source row must remain an output row; mapped columns still retain their native vertical merges. Select a merged record column only when every mapping into one target cell has one equal scalar value throughout that merge group. Map source grid columns onto every target prototype cell. The provider concatenates source records in declared order, copies each mapped source cell in full, preserves horizontal spans, rebuilds vertical merges only within each source range, validates the complete target grid, commits once, and returns structural readback. It does not discover source tables, choose source or target business meaning, filter records, translate or rewrite text, or apply business-specific rules. A single source uses the same sources array with one item. If a target needs only selected source descendants, such as one language from a bilingual cell, immediately use the returned target-cell addresses with docx_replace_content_from_source before releasing that source or reading back the completed target.',
435
496
  inputSchema: inputContract('docx_fill_table_from_tables'),
436
497
  outputSchema: fixedEditOutput('docx_fill_table_from_tables'),
437
498
  handler: args => fixedEdit('docx_fill_table_from_tables', args),
@@ -488,7 +549,7 @@ const tools = [
488
549
  },
489
550
  {
490
551
  name: 'docx_export_json',
491
- description: 'Produce a body-only DOCX JSON projection only when a downstream consumer explicitly requires that format. 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. This does not replace bounded list and read operations.',
552
+ description: 'Produce the complete body-only DOCX JSON projection required for final-document readback or another downstream consumer of that format. 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. This does not replace bounded list and read operations during document processing.',
492
553
  inputSchema: inputContract('docx_export_json'),
493
554
  outputSchema: largeResultOutput('docx_export_json'),
494
555
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -580,6 +641,14 @@ const tools = [
580
641
  annotations: { readOnlyHint: true, idempotentHint: true },
581
642
  handler: xlsxExportJson,
582
643
  },
644
+ {
645
+ name: 'xlsx_read_range',
646
+ description: 'Read one explicit native A1 cell or rectangular range from one current XLSX worksheet. Pages use a row-major cell offset and return physical presence, raw and formatted values, normalized value type, formula metadata, style, rich text, and merged-range ownership. The receipt always reports remaining cells and the next offset. Continue only when another selected cell is needed. Set returnContent true to return the selected page when it fits the response limit. Provide output to store the same complete selected page as an immutable artifact. These channels are independent and may be used together; at least one is required. This tool does not infer regions, headers, records, field meanings, or business mappings; convert legacy XLS before reading Open XML cells.',
647
+ inputSchema: inputContract('xlsx_read_range'),
648
+ outputSchema: xlsxRangeReadOutput,
649
+ annotations: { readOnlyHint: true, idempotentHint: true },
650
+ handler: xlsxReadRange,
651
+ },
583
652
  ...fixedToolDefinitions(xlsxFixedTools),
584
653
  {
585
654
  name: 'xlsx_validate',
@@ -692,32 +761,17 @@ function compactDocxInspection(report) {
692
761
  };
693
762
  }
694
763
 
695
- function compactDocxObjectIdentity(object) {
696
- return {
697
- address: object.address,
698
- parentAddress: object.parentAddress,
699
- kind: object.kind,
700
- textPreview: object.textPreview,
701
- gridSpan: object.gridSpan,
702
- verticalMerge: object.verticalMerge,
703
- };
704
- }
705
-
706
764
  function compactTableIndexEntry(table) {
707
- const paragraph = value => value === null ? null : {
708
- address: value.address,
709
- textPreview: value.textPreview.trim().replace(/\s+/gu, ' ').slice(0, 160),
710
- textLength: value.textLength,
711
- };
765
+ const paragraphText = value => value === null
766
+ ? null
767
+ : value.textPreview.trim().replace(/\s+/gu, ' ').slice(0, 32);
712
768
  return {
713
769
  address: table.address,
714
- parentAddress: table.parentAddress,
715
770
  rowCount: table.rowCount,
716
771
  columnCount: table.columnCount,
717
- textPreview: table.textPreview.trim().replace(/\s+/gu, ' ').slice(0, 240),
718
- textLength: table.textLength,
719
- precedingParagraph: paragraph(table.precedingParagraph),
720
- followingParagraph: paragraph(table.followingParagraph),
772
+ textPreview: table.textPreview.trim().replace(/\s+/gu, ' ').slice(0, 64),
773
+ precedingText: paragraphText(table.precedingParagraph),
774
+ followingText: paragraphText(table.followingParagraph),
721
775
  };
722
776
  }
723
777
 
@@ -734,6 +788,7 @@ function compactDocxObservation(observation) {
734
788
  ...(identity.verticalMerge === null ? {} : { verticalMerge: identity.verticalMerge }),
735
789
  ...(node.object.verticalMergeOwner === null ? {} : { verticalMergeOwner: node.object.verticalMergeOwner }),
736
790
  ...(node.object.logicalText === null ? {} : { logicalText: node.object.logicalText }),
791
+ ...(identity.verticalTextAlignment === null ? {} : { verticalTextAlignment: identity.verticalTextAlignment }),
737
792
  };
738
793
  return {
739
794
  object,
@@ -755,7 +810,7 @@ async function docxInspect(args) {
755
810
  artifact: delivery.output === null
756
811
  ? null
757
812
  : await writeIdempotentJsonArtifact(delivery.output, result.json),
758
- ...(delivery.returnContent ? compactDocxInspection(result.json) : {}),
813
+ ...compactDocxInspection(result.json),
759
814
  };
760
815
  }
761
816
 
@@ -829,9 +884,8 @@ async function docxObservation(tool, args) {
829
884
  };
830
885
  }
831
886
  const offset = Math.min(args.offset ?? 0, totalCount);
832
- const requestedLimit = args.limit ?? totalCount;
833
887
  const tables = [];
834
- for (const sourceTable of payload.tables.slice(offset, offset + requestedLimit)) {
888
+ for (const sourceTable of payload.tables.slice(offset)) {
835
889
  const table = compactTableIndexEntry(sourceTable);
836
890
  const candidate = [...tables, table];
837
891
  if (tables.length > 0
@@ -1217,6 +1271,21 @@ async function xlsxExportJson(args) {
1217
1271
  return deliverLargeJsonResult({ tool: 'xlsx_export_json', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1218
1272
  }
1219
1273
 
1274
+ async function xlsxReadRange(args) {
1275
+ const input = path.resolve(requireString(args.input, 'input'));
1276
+ return withTempJsonFile(args, async requestPath => {
1277
+ const result = await runJsonCandidateChain(xlsxCandidates, ['xlsx_read_range', requestPath]);
1278
+ return deliverLargeJsonResult({
1279
+ tool: 'xlsx_read_range',
1280
+ args,
1281
+ runtime: commandRuntime(result),
1282
+ payload: result.json,
1283
+ sourcePaths: [input],
1284
+ summary: result.json.receipt,
1285
+ });
1286
+ });
1287
+ }
1288
+
1220
1289
  async function xlsxValidate(args) {
1221
1290
  const input = path.resolve(requireString(args.input, 'input'));
1222
1291
  const result = await runJsonCandidateChain(xlsxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.21.22",
3
+ "version": "0.21.28",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  "_shared/large-json-result.mjs",
21
21
  "_shared/output-write-lock.mjs",
22
22
  "office/index.mjs",
23
+ "office/docx-object-identity.mjs",
23
24
  "office/README.md",
24
25
  "office/contracts/tiwater-office-provider-contract-manifest-v1.json",
25
26
  "office/contracts/*.schema.json"