xlsx-for-ai 3.0.13 → 3.0.16

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.
Files changed (2) hide show
  1. package/mcp.js +204 -41
  2. package/package.json +1 -1
package/mcp.js CHANGED
@@ -39,7 +39,7 @@ const TOOLS = [
39
39
  inputSchema: {
40
40
  type: 'object',
41
41
  properties: {
42
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
42
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
43
43
  format: {
44
44
  type: 'string',
45
45
  enum: ['md', 'json', 'sql'],
@@ -64,7 +64,7 @@ const TOOLS = [
64
64
  inputSchema: {
65
65
  type: 'object',
66
66
  properties: {
67
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
67
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
68
68
  },
69
69
  required: ['file_path'],
70
70
  },
@@ -80,7 +80,7 @@ const TOOLS = [
80
80
  inputSchema: {
81
81
  type: 'object',
82
82
  properties: {
83
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
83
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
84
84
  sheet: { type: 'string', description: 'Limit to one sheet (default: all).' },
85
85
  },
86
86
  required: ['file_path'],
@@ -97,8 +97,8 @@ const TOOLS = [
97
97
  inputSchema: {
98
98
  type: 'object',
99
99
  properties: {
100
- file_path_a: { type: 'string', description: 'Path to the base .xlsx file.' },
101
- file_path_b: { type: 'string', description: 'Path to the changed .xlsx file.' },
100
+ file_path_a: { type: 'string', description: 'Absolute path to the base .xlsx file. Pass the path string AS-IS — do NOT base64-encode the file; the client reads it.' },
101
+ file_path_b: { type: 'string', description: 'Absolute path to the changed .xlsx file. Pass the path string AS-IS — do NOT base64-encode the file; the client reads it.' },
102
102
  sheet: { type: 'string', description: 'Limit diff to one sheet (default: all).' },
103
103
  },
104
104
  required: ['file_path_a', 'file_path_b'],
@@ -181,7 +181,7 @@ const TOOLS = [
181
181
  },
182
182
  spec_path: {
183
183
  type: 'string',
184
- description: 'Path to a .json file carrying the spec (alternative to inline spec for large workbooks).',
184
+ description: 'Absolute path to a .json file carrying the spec (alternative to inline spec for large workbooks). Pass the path string AS-IS — do NOT base64-encode; the client reads it.',
185
185
  },
186
186
  out_path: {
187
187
  type: 'string',
@@ -189,7 +189,7 @@ const TOOLS = [
189
189
  },
190
190
  base_file_b64: {
191
191
  type: 'string',
192
- description: 'Optional base64 of an existing .xlsx to edit-in-place. When omitted, a fresh workbook is created.',
192
+ description: 'NARROW EXCEPTION — xlsx_write ONLY accepts a base64-encoded base workbook here for edit-in-place. Every OTHER tool in this connector takes a file PATH (`file_path`), not bytes. When omitted, a fresh workbook is created.',
193
193
  },
194
194
  },
195
195
  // out_path is the typical caller's choice but not strictly required —
@@ -212,7 +212,7 @@ const TOOLS = [
212
212
  inputSchema: {
213
213
  type: 'object',
214
214
  properties: {
215
- file_path: { type: 'string', description: 'Absolute path to the source .xlsx file.' },
215
+ file_path: { type: 'string', description: 'Absolute path to the source .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O.' },
216
216
  out_path: { type: 'string', description: 'Destination for the redacted .xlsx file.' },
217
217
  },
218
218
  required: ['file_path', 'out_path'],
@@ -238,7 +238,7 @@ const TOOLS = [
238
238
  inputSchema: {
239
239
  type: 'object',
240
240
  properties: {
241
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
241
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
242
242
  sheet: { type: 'string', description: 'Sheet name (default: first sheet).' },
243
243
  header_row: { type: 'integer', description: 'Header row (1-based). 0 = treat row 1 as data, no header.' },
244
244
  max_rows: { type: 'integer', description: 'Max data rows to scan (default 10000).' },
@@ -258,7 +258,7 @@ const TOOLS = [
258
258
  inputSchema: {
259
259
  type: 'object',
260
260
  properties: {
261
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
261
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
262
262
  predicates: {
263
263
  type: 'array',
264
264
  minItems: 1,
@@ -292,7 +292,7 @@ const TOOLS = [
292
292
  inputSchema: {
293
293
  type: 'object',
294
294
  properties: {
295
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
295
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
296
296
  group_by: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Columns to group by.' },
297
297
  aggs: {
298
298
  type: 'array',
@@ -328,7 +328,7 @@ const TOOLS = [
328
328
  inputSchema: {
329
329
  type: 'object',
330
330
  properties: {
331
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
331
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
332
332
  },
333
333
  required: ['file_path'],
334
334
  },
@@ -344,7 +344,7 @@ const TOOLS = [
344
344
  inputSchema: {
345
345
  type: 'object',
346
346
  properties: {
347
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
347
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
348
348
  by: {
349
349
  type: 'array',
350
350
  minItems: 1,
@@ -376,7 +376,7 @@ const TOOLS = [
376
376
  inputSchema: {
377
377
  type: 'object',
378
378
  properties: {
379
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
379
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
380
380
  column: { type: 'string', description: 'Column to count values in.' },
381
381
  sheet: { type: 'string' },
382
382
  header_row: { type: 'integer' },
@@ -398,7 +398,7 @@ const TOOLS = [
398
398
  inputSchema: {
399
399
  type: 'object',
400
400
  properties: {
401
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
401
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
402
402
  sheet: { type: 'string', description: 'Filter to one sheet (default: all sheets).' },
403
403
  include_results: { type: 'boolean', description: 'Include cached results column (default true).' },
404
404
  limit: { type: 'integer', description: 'Max formulas to return (default 1000, max 5000).' },
@@ -418,7 +418,7 @@ const TOOLS = [
418
418
  inputSchema: {
419
419
  type: 'object',
420
420
  properties: {
421
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
421
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
422
422
  sheet: { type: 'string', description: 'Filter to one sheet (default: all sheets).' },
423
423
  include_columns: { type: 'boolean', description: 'Include column names per table (default true).' },
424
424
  },
@@ -436,7 +436,7 @@ const TOOLS = [
436
436
  inputSchema: {
437
437
  type: 'object',
438
438
  properties: {
439
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
439
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
440
440
  index: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Row-axis grouping columns.' },
441
441
  columns: { type: 'array', items: { type: 'string' }, description: 'Column-axis grouping columns (optional).' },
442
442
  values: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Columns to aggregate.' },
@@ -460,7 +460,7 @@ const TOOLS = [
460
460
  inputSchema: {
461
461
  type: 'object',
462
462
  properties: {
463
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
463
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
464
464
  formulas: {
465
465
  type: 'array',
466
466
  items: { type: 'string' },
@@ -488,7 +488,7 @@ const TOOLS = [
488
488
  inputSchema: {
489
489
  type: 'object',
490
490
  properties: {
491
- file_path: { type: 'string', description: 'Absolute path to the source spreadsheet file (any supported format).' },
491
+ file_path: { type: 'string', description: 'Absolute path to the source spreadsheet file (any supported format). Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O.' },
492
492
  to: {
493
493
  type: 'string',
494
494
  enum: [
@@ -516,7 +516,7 @@ const TOOLS = [
516
516
  inputSchema: {
517
517
  type: 'object',
518
518
  properties: {
519
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
519
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
520
520
  mode: {
521
521
  type: 'string',
522
522
  enum: ['diagnose', 'execute'],
@@ -577,7 +577,7 @@ const TOOLS = [
577
577
  inputSchema: {
578
578
  type: 'object',
579
579
  properties: {
580
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
580
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
581
581
  },
582
582
  required: ['file_path'],
583
583
  },
@@ -594,7 +594,7 @@ const TOOLS = [
594
594
  inputSchema: {
595
595
  type: 'object',
596
596
  properties: {
597
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
597
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
598
598
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
599
599
  },
600
600
  required: ['file_path'],
@@ -612,7 +612,7 @@ const TOOLS = [
612
612
  inputSchema: {
613
613
  type: 'object',
614
614
  properties: {
615
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
615
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
616
616
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
617
617
  },
618
618
  required: ['file_path'],
@@ -630,7 +630,7 @@ const TOOLS = [
630
630
  inputSchema: {
631
631
  type: 'object',
632
632
  properties: {
633
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
633
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
634
634
  },
635
635
  required: ['file_path'],
636
636
  },
@@ -647,7 +647,7 @@ const TOOLS = [
647
647
  inputSchema: {
648
648
  type: 'object',
649
649
  properties: {
650
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
650
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
651
651
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
652
652
  },
653
653
  required: ['file_path'],
@@ -665,7 +665,7 @@ const TOOLS = [
665
665
  inputSchema: {
666
666
  type: 'object',
667
667
  properties: {
668
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
668
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
669
669
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
670
670
  },
671
671
  required: ['file_path'],
@@ -682,7 +682,7 @@ const TOOLS = [
682
682
  inputSchema: {
683
683
  type: 'object',
684
684
  properties: {
685
- file_path: { type: 'string', description: 'Absolute path to the .xlsx / .xlsm file.' },
685
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx / .xlsm file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O.' },
686
686
  },
687
687
  required: ['file_path'],
688
688
  },
@@ -699,7 +699,7 @@ const TOOLS = [
699
699
  inputSchema: {
700
700
  type: 'object',
701
701
  properties: {
702
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
702
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
703
703
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
704
704
  },
705
705
  required: ['file_path'],
@@ -716,7 +716,7 @@ const TOOLS = [
716
716
  inputSchema: {
717
717
  type: 'object',
718
718
  properties: {
719
- file_path: { type: 'string', description: 'Absolute path to the .xlsx / .xlsm / .xlsb file.' },
719
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx / .xlsm / .xlsb file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O.' },
720
720
  },
721
721
  required: ['file_path'],
722
722
  },
@@ -733,7 +733,7 @@ const TOOLS = [
733
733
  inputSchema: {
734
734
  type: 'object',
735
735
  properties: {
736
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
736
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
737
737
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
738
738
  },
739
739
  required: ['file_path'],
@@ -750,7 +750,7 @@ const TOOLS = [
750
750
  inputSchema: {
751
751
  type: 'object',
752
752
  properties: {
753
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
753
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
754
754
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
755
755
  },
756
756
  required: ['file_path'],
@@ -768,7 +768,7 @@ const TOOLS = [
768
768
  inputSchema: {
769
769
  type: 'object',
770
770
  properties: {
771
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
771
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
772
772
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
773
773
  },
774
774
  required: ['file_path'],
@@ -785,7 +785,7 @@ const TOOLS = [
785
785
  inputSchema: {
786
786
  type: 'object',
787
787
  properties: {
788
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
788
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
789
789
  },
790
790
  required: ['file_path'],
791
791
  },
@@ -802,7 +802,7 @@ const TOOLS = [
802
802
  inputSchema: {
803
803
  type: 'object',
804
804
  properties: {
805
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
805
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
806
806
  },
807
807
  required: ['file_path'],
808
808
  },
@@ -818,7 +818,7 @@ const TOOLS = [
818
818
  inputSchema: {
819
819
  type: 'object',
820
820
  properties: {
821
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
821
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
822
822
  },
823
823
  required: ['file_path'],
824
824
  },
@@ -834,7 +834,7 @@ const TOOLS = [
834
834
  inputSchema: {
835
835
  type: 'object',
836
836
  properties: {
837
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
837
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
838
838
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
839
839
  },
840
840
  required: ['file_path'],
@@ -851,7 +851,7 @@ const TOOLS = [
851
851
  inputSchema: {
852
852
  type: 'object',
853
853
  properties: {
854
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
854
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
855
855
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
856
856
  },
857
857
  required: ['file_path'],
@@ -868,7 +868,7 @@ const TOOLS = [
868
868
  inputSchema: {
869
869
  type: 'object',
870
870
  properties: {
871
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
871
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
872
872
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
873
873
  },
874
874
  required: ['file_path'],
@@ -885,7 +885,7 @@ const TOOLS = [
885
885
  inputSchema: {
886
886
  type: 'object',
887
887
  properties: {
888
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
888
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
889
889
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
890
890
  },
891
891
  required: ['file_path'],
@@ -903,7 +903,7 @@ const TOOLS = [
903
903
  inputSchema: {
904
904
  type: 'object',
905
905
  properties: {
906
- file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
906
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file. Pass the path string AS-IS — do NOT read, open, or base64-encode the file; the client handles all file I/O. The base64 surface in this connector is OUTPUT-only (_meta.file_b64).' },
907
907
  sheet: { type: 'string', description: 'Optional: restrict to a specific sheet.' },
908
908
  detailed: { type: 'boolean', description: 'If true, return per-cell breakdown (capped at 1000 cells). Default false (per-sheet rollup).' },
909
909
  },
@@ -1501,6 +1501,21 @@ function friendlyErrorMessage(toolName, err) {
1501
1501
  return `${toolName}: free-tier monthly cap reached — see xlsx-for-ai.dev/pricing.`;
1502
1502
  case 'FALLBACK_ENGINE_MISSING':
1503
1503
  return `${toolName}: local fallback engine not installed (\`npm install @protobi/exceljs\`).`;
1504
+ case 'BASE64_MISREAD':
1505
+ // SPM SPEC base64-defensive-error-and-suggested-next-call — turn the
1506
+ // base64-bash-hang class into a one-turn recovery. The error names
1507
+ // the offending field AND restates the contract so the next call
1508
+ // self-corrects.
1509
+ return (
1510
+ `${toolName}: argument "${err.field || 'file_path'}" looks like base64-encoded bytes, not a file path. ` +
1511
+ `This tool takes a PATH STRING (e.g. "/Users/you/foo.xlsx" or "~/Desktop/foo.xlsx"); the client reads and encodes the file for you. ` +
1512
+ `Retry with file_path set to the path string.`
1513
+ );
1514
+ case 'MISSING_REQUIRED_ARG':
1515
+ return (
1516
+ `${toolName}: missing required argument "${err.field || ''}". ` +
1517
+ `Check the tool's input schema; the workhorse case is file_path as a path string (NOT bytes).`
1518
+ );
1504
1519
  default:
1505
1520
  break;
1506
1521
  }
@@ -1559,7 +1574,148 @@ function friendlyErrorMessage(toolName, err) {
1559
1574
  // Tool dispatch
1560
1575
  // ---------------------------------------------------------------------------
1561
1576
 
1577
+ // ---------------------------------------------------------------------------
1578
+ // Defensive input-contract validation (belt-and-suspenders for SPM SPEC
1579
+ // base64-defensive-error-and-suggested-next-call).
1580
+ //
1581
+ // Description hardening in 3.0.14 reduces the rate at which the model
1582
+ // invents a base64-encoding step, but doesn't eliminate it. If a tool
1583
+ // call arrives with byte-shaped content where a file_path is expected,
1584
+ // or with file_path missing entirely, throw a crisp error code the
1585
+ // friendlyErrorMessage path translates into actionable text — turning
1586
+ // the prior indefinite hang into a one-turn recovery.
1587
+ // ---------------------------------------------------------------------------
1588
+
1589
+ const FILE_PATH_FIELDS = new Set(['file_path', 'file_path_a', 'file_path_b', 'spec_path']);
1590
+ const BASE64_ONLY_REGEX = /^[A-Za-z0-9+/]+=*$/;
1591
+
1592
+ function looksLikeBase64(value) {
1593
+ if (typeof value !== 'string') return false;
1594
+ // The trap: `/` is a base64-alphabet character AND a POSIX path
1595
+ // separator, so "contains slash" can't distinguish on its own. Real
1596
+ // file paths carry distinctive markers base64 strings don't:
1597
+ // - `.` for an extension (any spreadsheet path ends with .xlsx/.xls/
1598
+ // etc; intermediate dirs often have them too)
1599
+ // - `\` for Windows path separators
1600
+ // - `~` for home prefix
1601
+ // - spaces (common in Mac/Windows user dirs)
1602
+ // If ANY of those appear, we treat the string as a path.
1603
+ if (value.length < 200) return false;
1604
+ if (
1605
+ value.includes('.') ||
1606
+ value.includes('\\') ||
1607
+ value.includes('~') ||
1608
+ value.includes(' ')
1609
+ ) {
1610
+ return false;
1611
+ }
1612
+ const trimmed = value.trim();
1613
+ return BASE64_ONLY_REGEX.test(trimmed);
1614
+ }
1615
+
1616
+ function validateToolArgs(name, args) {
1617
+ // Find the tool's inputSchema so we can read the `required` array. Use a
1618
+ // local Map lookup so a 50-tool catalog isn't re-scanned per call.
1619
+ if (!validateToolArgs._toolByName) {
1620
+ validateToolArgs._toolByName = new Map(TOOLS.map((t) => [t.name, t]));
1621
+ }
1622
+ const tool = validateToolArgs._toolByName.get(name);
1623
+ if (!tool || !tool.inputSchema) return; // unknown tool — server will route or fail
1624
+ const schema = tool.inputSchema;
1625
+ const required = Array.isArray(schema.required) ? schema.required : [];
1626
+ const argsObj = args && typeof args === 'object' ? args : {};
1627
+
1628
+ // Required-field presence check.
1629
+ for (const field of required) {
1630
+ const v = argsObj[field];
1631
+ if (v === undefined || v === null || (typeof v === 'string' && v.length === 0)) {
1632
+ const err = new Error(`${name}: missing required argument "${field}".`);
1633
+ err.code = 'MISSING_REQUIRED_ARG';
1634
+ err.field = field;
1635
+ throw err;
1636
+ }
1637
+ }
1638
+
1639
+ // Base64-misread check on every file_path-shaped field.
1640
+ for (const field of FILE_PATH_FIELDS) {
1641
+ if (!(field in argsObj)) continue;
1642
+ if (looksLikeBase64(argsObj[field])) {
1643
+ const err = new Error(
1644
+ `${name}: argument "${field}" looks like base64-encoded bytes, not a file path.`
1645
+ );
1646
+ err.code = 'BASE64_MISREAD';
1647
+ err.field = field;
1648
+ throw err;
1649
+ }
1650
+ }
1651
+ }
1652
+
1653
+ // ---------------------------------------------------------------------------
1654
+ // Suggested-next-call injection on triage tools (SPM SPEC follow-up).
1655
+ //
1656
+ // xlsx_doctor's findings reference follow-on tools by name ("Run
1657
+ // xlsx_workbook_views to enumerate", "Run xlsx_external_links to see
1658
+ // the target"). Post-process the response text to add a concrete
1659
+ // invocation example for each referenced tool — pre-filled with the
1660
+ // caller's file_path. Doubles as a correct-usage exemplar (path-shaped,
1661
+ // no base64) that mitigates the misread class structurally.
1662
+ // ---------------------------------------------------------------------------
1663
+
1664
+ // Tools that take ONLY a required file_path — safe to suggest with a
1665
+ // one-arg invocation.
1666
+ const _drillDownEligible = (() => {
1667
+ const map = new Map();
1668
+ for (const t of TOOLS) {
1669
+ const req = Array.isArray(t.inputSchema?.required) ? t.inputSchema.required : [];
1670
+ if (req.length === 1 && req[0] === 'file_path') map.set(t.name, true);
1671
+ }
1672
+ return map;
1673
+ })();
1674
+
1675
+ function injectDrillDownExamples(result, callerToolName, args) {
1676
+ if (!result || typeof result !== 'object') return result;
1677
+ if (!args || typeof args.file_path !== 'string' || args.file_path.length === 0) return result;
1678
+ const content = Array.isArray(result.content) ? result.content : null;
1679
+ if (!content) return result;
1680
+ // First text block holds the rendered findings.
1681
+ const firstText = content.find((c) => c && c.type === 'text' && typeof c.text === 'string');
1682
+ if (!firstText) return result;
1683
+
1684
+ // Scan for xlsx_FOO mentions OTHER than the caller itself.
1685
+ const mentioned = new Set();
1686
+ const re = /\bxlsx_[a-z_]+(?:_[a-z_]+)*\b/g;
1687
+ let m;
1688
+ while ((m = re.exec(firstText.text)) !== null) {
1689
+ const tool = m[0];
1690
+ if (tool === callerToolName) continue;
1691
+ if (_drillDownEligible.has(tool)) mentioned.add(tool);
1692
+ }
1693
+ if (mentioned.size === 0) return result;
1694
+
1695
+ const filePathJson = JSON.stringify(args.file_path);
1696
+ const lines = [...mentioned].sort().map(
1697
+ (toolName) => `- \`${toolName}({ "file_path": ${filePathJson} })\``
1698
+ );
1699
+ const footer =
1700
+ '\n\n---\nDrill-down suggestions — concrete invocations pre-filled with your file_path ' +
1701
+ '(pass the path STRING, not file bytes; the client reads the file):\n' +
1702
+ lines.join('\n');
1703
+
1704
+ // Return a new result object with the footer appended; do NOT mutate the
1705
+ // server's response.
1706
+ const newContent = content.map((c) => {
1707
+ if (c === firstText) return { ...c, text: c.text + footer };
1708
+ return c;
1709
+ });
1710
+ return { ...result, content: newContent };
1711
+ }
1712
+
1562
1713
  async function dispatchTool(name, args) {
1714
+ // Defensive validation — runs first so a base64 misread or missing
1715
+ // file_path produces an actionable error instead of an opaque server
1716
+ // failure or a base64-bash-hang.
1717
+ validateToolArgs(name, args);
1718
+
1563
1719
  // xlsx_read: relay to API; fallback to local on unreachable / 5xx
1564
1720
  if (name === 'xlsx_read') {
1565
1721
  const body = {
@@ -1954,7 +2110,14 @@ async function dispatchTool(name, args) {
1954
2110
  file_b64: fileToB64(args.file_path),
1955
2111
  options: opts,
1956
2112
  };
1957
- return callTool(name, body);
2113
+ const result = await callTool(name, body);
2114
+
2115
+ // Triage tools that mention follow-on tools in their findings get a
2116
+ // drill-down footer with pre-filled invocations. xlsx_doctor is the
2117
+ // primary triage surface; xlsx_topology and xlsx_doctor's siblings can
2118
+ // benefit too, so we run the injection universally — it's a no-op on
2119
+ // any response whose text doesn't mention other xlsx_* tools.
2120
+ return injectDrillDownExamples(result, name, args);
1958
2121
  }
1959
2122
 
1960
2123
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "xlsx-for-ai",
3
3
  "mcpName": "io.github.senoff/xlsx-for-ai",
4
- "version": "3.0.13",
4
+ "version": "3.0.16",
5
5
  "description": "The MCP server that makes LLMs reliable on real-world Excel spreadsheets. Thin npm client over a hosted API — read, write, diff, redact, and supervise .xlsx files from any MCP-aware agent.",
6
6
  "main": "index.js",
7
7
  "bin": {