xlsx-for-ai 2.0.0-beta.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/index.js +11 -1
- package/lib/client.js +6 -0
- package/mcp.js +347 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,13 @@ Add `xlsx-for-ai` as a tool server in your agent runtime. First invocation auto-
|
|
|
18
18
|
|
|
19
19
|
### Claude Desktop
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
**Easiest: one-click install via the `.mcpb` bundle.** Download and drag into Claude Desktop (Settings → Extensions):
|
|
22
|
+
|
|
23
|
+
**[xlsx-for-ai-2.0.0.mcpb](https://github.com/senoff/xlsx-for-ai/releases/latest/download/xlsx-for-ai-2.0.0.mcpb)** *(latest release)*
|
|
24
|
+
|
|
25
|
+
The bundle includes the full npm package and registers all MCP tools automatically. No manual config edits needed.
|
|
26
|
+
|
|
27
|
+
**Or: hand-edit the config file** at `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
22
28
|
|
|
23
29
|
```json
|
|
24
30
|
{
|
|
@@ -30,7 +36,7 @@ Config file: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
|
30
36
|
}
|
|
31
37
|
```
|
|
32
38
|
|
|
33
|
-
Verify: restart Claude Desktop, open a new conversation, and ask "what MCP tools do you have?" — `xlsx_read`, `xlsx_list_sheets`, `xlsx_schema`, `xlsx_diff`, `xlsx_write`, and `xlsx_redact` should appear.
|
|
39
|
+
Verify either path: restart Claude Desktop, open a new conversation, and ask "what MCP tools do you have?" — `xlsx_read`, `xlsx_list_sheets`, `xlsx_schema`, `xlsx_diff`, `xlsx_write`, and `xlsx_redact` should appear.
|
|
34
40
|
|
|
35
41
|
### Cursor
|
|
36
42
|
|
|
@@ -251,6 +257,19 @@ xlsx-for-ai --disable-telemetry
|
|
|
251
257
|
xlsx-for-ai --telemetry-status
|
|
252
258
|
```
|
|
253
259
|
|
|
260
|
+
**Privacy strict mode** — prevents error-triggered capture of your workbook bytes (see [PRIVACY.md](PRIVACY.md)):
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Per-session flag (applies to all tool calls in the CLI invocation)
|
|
264
|
+
xlsx-for-ai --privacy=strict myfile.xlsx
|
|
265
|
+
|
|
266
|
+
# Environment variable (applies globally to all requests in the process)
|
|
267
|
+
XFA_PRIVACY=strict xlsx-for-ai myfile.xlsx
|
|
268
|
+
|
|
269
|
+
# In MCP server config (applies to all tool calls from the MCP server):
|
|
270
|
+
# Set XFA_PRIVACY=strict in your MCP server's env block
|
|
271
|
+
```
|
|
272
|
+
|
|
254
273
|
Delete the config to reset your client ID and API key:
|
|
255
274
|
|
|
256
275
|
```bash
|
package/index.js
CHANGED
|
@@ -31,7 +31,8 @@ const {
|
|
|
31
31
|
|
|
32
32
|
function parseArgs(argv) {
|
|
33
33
|
const opts = { file: null, format: 'text', sheet: null, evaluate: false,
|
|
34
|
-
telemetryStatus: false, enableTelemetry: false, disableTelemetry: false
|
|
34
|
+
telemetryStatus: false, enableTelemetry: false, disableTelemetry: false,
|
|
35
|
+
privacyStrict: false, showVersion: false };
|
|
35
36
|
let i = 0;
|
|
36
37
|
while (i < argv.length) {
|
|
37
38
|
const a = argv[i];
|
|
@@ -42,6 +43,8 @@ function parseArgs(argv) {
|
|
|
42
43
|
else if (a === '--telemetry-status') opts.telemetryStatus = true;
|
|
43
44
|
else if (a === '--enable-telemetry') opts.enableTelemetry = true;
|
|
44
45
|
else if (a === '--disable-telemetry') opts.disableTelemetry = true;
|
|
46
|
+
else if (a === '--privacy=strict') opts.privacyStrict = true;
|
|
47
|
+
else if (a === '--version' || a === '-v') opts.showVersion = true;
|
|
45
48
|
else if (!a.startsWith('--')) opts.file = a;
|
|
46
49
|
i++;
|
|
47
50
|
}
|
|
@@ -55,6 +58,7 @@ function parseArgs(argv) {
|
|
|
55
58
|
async function main() {
|
|
56
59
|
const opts = parseArgs(process.argv.slice(2));
|
|
57
60
|
|
|
61
|
+
if (opts.showVersion) { console.log(require('./package.json').version); return; }
|
|
58
62
|
if (opts.telemetryStatus) { console.log(telemetryStatus()); return; }
|
|
59
63
|
if (opts.enableTelemetry) { enableTelemetry(); console.log('Telemetry enabled.'); return; }
|
|
60
64
|
if (opts.disableTelemetry) { disableTelemetry(); console.log('Telemetry disabled.'); return; }
|
|
@@ -72,6 +76,12 @@ async function main() {
|
|
|
72
76
|
|
|
73
77
|
await ensureRegistered();
|
|
74
78
|
|
|
79
|
+
// Privacy strict: --privacy=strict flag sets the env var for this process
|
|
80
|
+
// so callTool() (which reads XFA_PRIVACY) adds the header automatically.
|
|
81
|
+
if (opts.privacyStrict) {
|
|
82
|
+
process.env.XFA_PRIVACY = 'strict';
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
const fileB64 = fs.readFileSync(absPath).toString('base64');
|
|
76
86
|
// Server format enum is 'md' | 'json' | 'sql'. The legacy CLI default 'text'
|
|
77
87
|
// maps to the server's default (md). Don't send 'text' — server rejects it.
|
package/lib/client.js
CHANGED
|
@@ -39,6 +39,12 @@ async function post(path, body, opts = {}) {
|
|
|
39
39
|
if (cfg && cfg.api_key) headers['Authorization'] = `Bearer ${cfg.api_key}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Privacy opt-out: XFA_PRIVACY=strict env var (or per-call override) adds
|
|
43
|
+
// X-XFA-Privacy: strict to every request, preventing error-triggered capture.
|
|
44
|
+
if (process.env.XFA_PRIVACY === 'strict' || opts.privacyStrict) {
|
|
45
|
+
headers['X-XFA-Privacy'] = 'strict';
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
let res;
|
|
43
49
|
try {
|
|
44
50
|
res = await fetchWithTimeout(url, {
|
package/mcp.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* xlsx-for-ai MCP stdio server (2.0)
|
|
6
6
|
*
|
|
7
|
-
* Registers
|
|
7
|
+
* Registers 16 tools and relays each tools/call to the hosted API.
|
|
8
8
|
* xlsx_read falls back to local engine if API is unreachable (5xx / timeout).
|
|
9
9
|
* All other tools fail with a clear "needs API connectivity" error.
|
|
10
10
|
*/
|
|
@@ -153,6 +153,263 @@ const TOOLS = [
|
|
|
153
153
|
required: ['file_path', 'out_path'],
|
|
154
154
|
},
|
|
155
155
|
},
|
|
156
|
+
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
// Pandas-shaped analysis tools — work where pandas can't:
|
|
159
|
+
// - preserves merged cells, named ranges, conditional formatting
|
|
160
|
+
// - reads workbooks with cross-engine validation (some tools)
|
|
161
|
+
// - dtype inference reports confidence per column instead of guessing
|
|
162
|
+
// All free-tier; the 10k/month cap is the throttle, not tier-gating.
|
|
163
|
+
// -------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
name: 'xlsx_describe',
|
|
167
|
+
description:
|
|
168
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
169
|
+
'This tool: pandas-style df.describe() per column — count, nulls, unique, min/max/mean/std for numerics, dtype with purity score.\n' +
|
|
170
|
+
'Unlike pandas.read_excel followed by df.describe(), this does not silently flatten merged cells or drop named ranges.\n\n' +
|
|
171
|
+
'USE WHEN: the user wants a quick summary of a LOCAL .xlsx file — "what\'s in this data?". ' +
|
|
172
|
+
'Returns a markdown table with one row per column. Faster + more structured than dumping full contents through xlsx_read.\n\n' +
|
|
173
|
+
'DO NOT USE WHEN: the user uploaded a file via paperclip/attach (built-in skill). Or for in-memory data the agent already holds.',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
178
|
+
sheet: { type: 'string', description: 'Sheet name (default: first sheet).' },
|
|
179
|
+
header_row: { type: 'integer', description: 'Header row (1-based). 0 = treat row 1 as data, no header.' },
|
|
180
|
+
max_rows: { type: 'integer', description: 'Max data rows to scan (default 10000).' },
|
|
181
|
+
},
|
|
182
|
+
required: ['file_path'],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
name: 'xlsx_filter',
|
|
188
|
+
description:
|
|
189
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
190
|
+
'This tool: pandas-style row filter on a LOCAL .xlsx file with predicates AND-combined: eq/ne/gt/gte/lt/lte/contains/in/is_null/not_null.\n' +
|
|
191
|
+
'Operates on real cell values — formulas evaluated server-side, not the cached results that pandas trusts blindly.\n\n' +
|
|
192
|
+
'USE WHEN: the user asks for "rows where X" / "show me only Y" against a LOCAL .xlsx file. ' +
|
|
193
|
+
'Returns matching rows as a markdown table, capped at 1000 rows by default with the actual match count.\n\n' +
|
|
194
|
+
'DO NOT USE WHEN: the user wants raw access to all rows (use xlsx_read). Or when the file came from an upload.',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
199
|
+
predicates: {
|
|
200
|
+
type: 'array',
|
|
201
|
+
minItems: 1,
|
|
202
|
+
description: 'AND-combined filter predicates. Each: { column, op, value } where op is eq/ne/gt/gte/lt/lte/contains/not_contains/in/not_in/is_null/not_null.',
|
|
203
|
+
items: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
required: ['column', 'op'],
|
|
206
|
+
properties: {
|
|
207
|
+
column: { type: 'string' },
|
|
208
|
+
op: { type: 'string', enum: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'contains', 'not_contains', 'in', 'not_in', 'is_null', 'not_null'] },
|
|
209
|
+
value: {},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
sheet: { type: 'string' },
|
|
214
|
+
header_row: { type: 'integer' },
|
|
215
|
+
limit: { type: 'integer' },
|
|
216
|
+
},
|
|
217
|
+
required: ['file_path', 'predicates'],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
name: 'xlsx_aggregate',
|
|
223
|
+
description:
|
|
224
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
225
|
+
'This tool: pandas-style df.groupby([cols]).agg({col: func}) on a LOCAL .xlsx file. funcs: sum / mean / min / max / count / count_distinct.\n' +
|
|
226
|
+
'Type-aware: numeric aggregations skip non-numeric values cleanly instead of pandas\' silent NaN promotion.\n\n' +
|
|
227
|
+
'USE WHEN: the user asks "what\'s the total / average / count of X by Y?" on a LOCAL .xlsx file. ' +
|
|
228
|
+
'Returns one row per group with the requested aggregations as a markdown table.\n\n' +
|
|
229
|
+
'DO NOT USE WHEN: the user wants to see individual rows (use xlsx_filter). Or for a 2D pivot (use xlsx_pivot).',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
234
|
+
group_by: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Columns to group by.' },
|
|
235
|
+
aggs: {
|
|
236
|
+
type: 'array',
|
|
237
|
+
minItems: 1,
|
|
238
|
+
description: 'Aggregations: { column, func, as? }.',
|
|
239
|
+
items: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
required: ['column', 'func'],
|
|
242
|
+
properties: {
|
|
243
|
+
column: { type: 'string' },
|
|
244
|
+
func: { type: 'string', enum: ['sum', 'mean', 'min', 'max', 'count', 'count_distinct'] },
|
|
245
|
+
as: { type: 'string' },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
sheet: { type: 'string' },
|
|
250
|
+
header_row: { type: 'integer' },
|
|
251
|
+
sort: { type: 'string', enum: ['asc', 'desc', 'none'] },
|
|
252
|
+
limit: { type: 'integer' },
|
|
253
|
+
},
|
|
254
|
+
required: ['file_path', 'group_by', 'aggs'],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
{
|
|
259
|
+
name: 'xlsx_named_ranges',
|
|
260
|
+
description:
|
|
261
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
262
|
+
'This tool: list all defined names (named ranges) in a LOCAL .xlsx workbook — name, scope (workbook or sheet), kind (cell / range / formula), reference.\n' +
|
|
263
|
+
'pandas.read_excel collapses named ranges into anonymous ranges; this tool surfaces them so the agent can reason about formulas like =NPV(DiscountRate, Cashflows) before reading data.\n\n' +
|
|
264
|
+
'USE WHEN: the agent is reasoning about a financial / engineering model and needs to know what cells named-range references resolve to. ' +
|
|
265
|
+
'Call before xlsx_read to orient.\n\n' +
|
|
266
|
+
'DO NOT USE WHEN: the workbook has no formulas (named ranges are mostly relevant for formula contexts). Or for upload/attached files.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
271
|
+
},
|
|
272
|
+
required: ['file_path'],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
name: 'xlsx_sort',
|
|
278
|
+
description:
|
|
279
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
280
|
+
'This tool: pandas-style df.sort_values() on a LOCAL .xlsx file with multi-column sort and per-column direction (asc/desc, default asc).\n' +
|
|
281
|
+
'Stable across all sort keys; type-aware comparison; nulls always sort last.\n\n' +
|
|
282
|
+
'USE WHEN: the user wants rows ordered by one or more columns. Returns the sorted rows as a markdown table.\n\n' +
|
|
283
|
+
'DO NOT USE WHEN: the data is already sorted as desired (use xlsx_read). Or for upload/attached files.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
288
|
+
by: {
|
|
289
|
+
type: 'array',
|
|
290
|
+
minItems: 1,
|
|
291
|
+
description: 'Sort keys: [{ column, direction? }]. direction defaults to asc.',
|
|
292
|
+
items: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
required: ['column'],
|
|
295
|
+
properties: {
|
|
296
|
+
column: { type: 'string' },
|
|
297
|
+
direction: { type: 'string', enum: ['asc', 'desc'] },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
sheet: { type: 'string' },
|
|
302
|
+
header_row: { type: 'integer' },
|
|
303
|
+
limit: { type: 'integer' },
|
|
304
|
+
},
|
|
305
|
+
required: ['file_path', 'by'],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
name: 'xlsx_value_counts',
|
|
311
|
+
description:
|
|
312
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
313
|
+
'This tool: pandas-style Series.value_counts() on one column of a LOCAL .xlsx file — count each unique value, sorted by frequency desc, with percentage.\n' +
|
|
314
|
+
'Excludes nulls by default; pass include_nulls=true to count them.\n\n' +
|
|
315
|
+
'USE WHEN: the user asks "what\'s the distribution of X?" / "how often does each value appear?". Returns a markdown table.\n\n' +
|
|
316
|
+
'DO NOT USE WHEN: the user wants groupby + multi-column aggregations (use xlsx_aggregate). Or for upload/attached files.',
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
properties: {
|
|
320
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
321
|
+
column: { type: 'string', description: 'Column to count values in.' },
|
|
322
|
+
sheet: { type: 'string' },
|
|
323
|
+
header_row: { type: 'integer' },
|
|
324
|
+
top_n: { type: 'integer', description: 'Show top N most-frequent values (default 50).' },
|
|
325
|
+
include_nulls: { type: 'boolean', description: 'Count null cells as a distinct value (default false).' },
|
|
326
|
+
},
|
|
327
|
+
required: ['file_path', 'column'],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
{
|
|
332
|
+
name: 'xlsx_formulas',
|
|
333
|
+
description:
|
|
334
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
335
|
+
'This tool: extract every formula in a LOCAL .xlsx workbook — cell coord (A1), formula text, cached result. openpyxl-style read-only metadata.\n' +
|
|
336
|
+
'Distinct from xlsx_read which returns evaluated values; this returns the formulas themselves so an agent can audit, transform, or rewrite them.\n\n' +
|
|
337
|
+
'USE WHEN: the user wants to see what formulas a workbook uses — spot-checking a model, auditing references, debugging unexpected results. ' +
|
|
338
|
+
'pandas cannot extract formulas; this is the only way for an agent to see them.\n\n' +
|
|
339
|
+
'DO NOT USE WHEN: the user wants computed values (use xlsx_read). Or for upload/attached files.',
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
344
|
+
sheet: { type: 'string', description: 'Filter to one sheet (default: all sheets).' },
|
|
345
|
+
include_results: { type: 'boolean', description: 'Include cached results column (default true).' },
|
|
346
|
+
limit: { type: 'integer', description: 'Max formulas to return (default 1000, max 5000).' },
|
|
347
|
+
},
|
|
348
|
+
required: ['file_path'],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
name: 'xlsx_tables',
|
|
354
|
+
description:
|
|
355
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
356
|
+
'This tool: list every Excel ListObject ("Format as Table" structures) in a LOCAL .xlsx workbook — name, sheet, range, header/totals flags, columns.\n' +
|
|
357
|
+
'pandas cannot see ListObjects; if a workbook uses Excel Tables, this is the only way to enumerate them.\n\n' +
|
|
358
|
+
'USE WHEN: the user references a "table" in a workbook by name, or you need to know what structured tables exist before reading. ' +
|
|
359
|
+
'Useful for workbooks with multiple tables on one sheet.\n\n' +
|
|
360
|
+
'DO NOT USE WHEN: the workbook has no Excel-Tables (just data ranges). Or for upload/attached files.',
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: 'object',
|
|
363
|
+
properties: {
|
|
364
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
365
|
+
sheet: { type: 'string', description: 'Filter to one sheet (default: all sheets).' },
|
|
366
|
+
include_columns: { type: 'boolean', description: 'Include column names per table (default true).' },
|
|
367
|
+
},
|
|
368
|
+
required: ['file_path'],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
{
|
|
373
|
+
name: 'xlsx_pivot',
|
|
374
|
+
description:
|
|
375
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
376
|
+
'This tool: pandas-style pivot_table() on a LOCAL .xlsx file — reshape a flat table into a 2D matrix where rows are unique values of `index`, columns are unique values of `columns`, and cells are an aggregation of `values`.\n' +
|
|
377
|
+
'agg modes: sum / mean / min / max / count / count_distinct. Optional fill_value for missing index×column combinations.\n\n' +
|
|
378
|
+
'USE WHEN: the user wants a cross-tab — "X by Y", "rows by columns" — that needs more than groupby. Returns a markdown table.\n\n' +
|
|
379
|
+
'DO NOT USE WHEN: there\'s only one grouping dimension (use xlsx_aggregate). Or for upload/attached files.',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
384
|
+
index: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Row-axis grouping columns.' },
|
|
385
|
+
columns: { type: 'array', items: { type: 'string' }, description: 'Column-axis grouping columns (optional).' },
|
|
386
|
+
values: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Columns to aggregate.' },
|
|
387
|
+
agg: { type: 'string', enum: ['sum', 'mean', 'min', 'max', 'count', 'count_distinct'], description: 'Aggregation function (default sum).' },
|
|
388
|
+
sheet: { type: 'string' },
|
|
389
|
+
header_row: { type: 'integer' },
|
|
390
|
+
fill_value: { description: 'Value (number or string) to use for missing index×column cells. Default empty string.' },
|
|
391
|
+
},
|
|
392
|
+
required: ['file_path', 'index', 'values'],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
{
|
|
397
|
+
name: 'xlsx_validate',
|
|
398
|
+
description:
|
|
399
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
400
|
+
'This tool: cross-engine consistency check on a LOCAL .xlsx file — runs the workbook through TWO independent renderers (@protobi/exceljs and @cj-tech-master/excelts) and reports cell-level divergences.\n' +
|
|
401
|
+
'No other tool can do this: pandas trusts cached values, openpyxl is single-engine, and Excel-itself disagrees with everything else on edge cases like LAMBDA, dynamic arrays, and timezone handling. xlsx_validate is the only way to know whether two engines agree on what your workbook says.\n\n' +
|
|
402
|
+
'USE WHEN: the user is about to send the workbook downstream for analysis or as an authoritative source — pre-flight check. Or for audit / regression testing across engine versions. ' +
|
|
403
|
+
'PAID — Bronze / Silver / Gold tier required.\n\n' +
|
|
404
|
+
'DO NOT USE WHEN: a casual read suffices (use xlsx_read). Or for upload/attached files.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
file_path: { type: 'string', description: 'Absolute path to the .xlsx file.' },
|
|
409
|
+
},
|
|
410
|
+
required: ['file_path'],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
156
413
|
];
|
|
157
414
|
|
|
158
415
|
// ---------------------------------------------------------------------------
|
|
@@ -255,6 +512,95 @@ async function dispatchTool(name, args) {
|
|
|
255
512
|
return applyFileB64(result, args.out_path);
|
|
256
513
|
}
|
|
257
514
|
|
|
515
|
+
// -------------------------------------------------------------------------
|
|
516
|
+
// Pandas-shaped tools — each builds its own server payload because the
|
|
517
|
+
// server expects different top-level fields per tool (predicates, group_by,
|
|
518
|
+
// by, column, index, etc.).
|
|
519
|
+
// -------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
if (name === 'xlsx_describe') {
|
|
522
|
+
return callTool('xlsx_describe', {
|
|
523
|
+
file_b64: fileToB64(args.file_path),
|
|
524
|
+
options: { sheet: args.sheet, header_row: args.header_row, max_rows: args.max_rows },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (name === 'xlsx_filter') {
|
|
529
|
+
return callTool('xlsx_filter', {
|
|
530
|
+
file_b64: fileToB64(args.file_path),
|
|
531
|
+
predicates: args.predicates,
|
|
532
|
+
options: { sheet: args.sheet, header_row: args.header_row, limit: args.limit },
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (name === 'xlsx_aggregate') {
|
|
537
|
+
return callTool('xlsx_aggregate', {
|
|
538
|
+
file_b64: fileToB64(args.file_path),
|
|
539
|
+
group_by: args.group_by,
|
|
540
|
+
aggs: args.aggs,
|
|
541
|
+
options: { sheet: args.sheet, header_row: args.header_row, sort: args.sort, limit: args.limit },
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (name === 'xlsx_named_ranges') {
|
|
546
|
+
return callTool('xlsx_named_ranges', {
|
|
547
|
+
file_b64: fileToB64(args.file_path),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (name === 'xlsx_sort') {
|
|
552
|
+
return callTool('xlsx_sort', {
|
|
553
|
+
file_b64: fileToB64(args.file_path),
|
|
554
|
+
by: args.by,
|
|
555
|
+
options: { sheet: args.sheet, header_row: args.header_row, limit: args.limit },
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (name === 'xlsx_value_counts') {
|
|
560
|
+
return callTool('xlsx_value_counts', {
|
|
561
|
+
file_b64: fileToB64(args.file_path),
|
|
562
|
+
column: args.column,
|
|
563
|
+
options: {
|
|
564
|
+
sheet: args.sheet,
|
|
565
|
+
header_row: args.header_row,
|
|
566
|
+
top_n: args.top_n,
|
|
567
|
+
include_nulls: args.include_nulls,
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (name === 'xlsx_formulas') {
|
|
573
|
+
return callTool('xlsx_formulas', {
|
|
574
|
+
file_b64: fileToB64(args.file_path),
|
|
575
|
+
options: { sheet: args.sheet, include_results: args.include_results, limit: args.limit },
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (name === 'xlsx_tables') {
|
|
580
|
+
return callTool('xlsx_tables', {
|
|
581
|
+
file_b64: fileToB64(args.file_path),
|
|
582
|
+
options: { sheet: args.sheet, include_columns: args.include_columns },
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (name === 'xlsx_pivot') {
|
|
587
|
+
const body = {
|
|
588
|
+
file_b64: fileToB64(args.file_path),
|
|
589
|
+
index: args.index,
|
|
590
|
+
values: args.values,
|
|
591
|
+
options: { sheet: args.sheet, header_row: args.header_row, fill_value: args.fill_value },
|
|
592
|
+
};
|
|
593
|
+
if (args.columns) body.columns = args.columns;
|
|
594
|
+
if (args.agg) body.agg = args.agg;
|
|
595
|
+
return callTool('xlsx_pivot', body);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (name === 'xlsx_validate') {
|
|
599
|
+
return callTool('xlsx_validate', {
|
|
600
|
+
file_b64: fileToB64(args.file_path),
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
258
604
|
// All other tools (list_sheets, schema) — single-file relay
|
|
259
605
|
const body = {
|
|
260
606
|
file_b64: fileToB64(args.file_path),
|
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": "2.
|
|
4
|
+
"version": "2.1.0",
|
|
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": {
|