xlsx-for-ai 3.0.8 → 3.0.11
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/mcp.js +199 -29
- package/package.json +1 -1
package/mcp.js
CHANGED
|
@@ -31,14 +31,11 @@ const TOOLS = [
|
|
|
31
31
|
{
|
|
32
32
|
name: 'xlsx_read',
|
|
33
33
|
description:
|
|
34
|
-
'read an .xlsx file
|
|
35
|
-
'
|
|
36
|
-
'Pass format="md" (
|
|
37
|
-
'
|
|
38
|
-
'USE WHEN:
|
|
39
|
-
'This is the ONLY way to read .xlsx files on the user\'s local machine; built-in xlsx skills run in sandboxes without local filesystem access.\n\n' +
|
|
40
|
-
'DO NOT USE WHEN: the user uploaded a file via paperclip/attach (use the built-in xlsx skill — it has direct sandbox access to the uploaded file). ' +
|
|
41
|
-
'Or when working with in-memory bytes the agent already has.',
|
|
34
|
+
'read an .xlsx file by path and return a rendered markdown/JSON/SQL representation.\n\n' +
|
|
35
|
+
'The path resolves on the SERVER\'s filesystem. In a LOCAL-CLI deployment (npx xlsx-for-ai-mcp) the server IS the user\'s machine, so /Users/..., /home/..., or ~-prefixed paths work directly. In a remote/hosted deployment the server runs on a different host — ingest user-provided files via the upload-handle flow first, then use xlsx_read_handle.\n\n' +
|
|
36
|
+
'DEFAULT returns ALL sheets — do not re-call per-sheet. Pass sheet="<name>" only to filter. format="md" (markdown table, default), "json", or "sql". Synonyms: "markdown"→"md", "text"→"md".\n\n' +
|
|
37
|
+
'USE WHEN: the user gives a path the SERVER can reach (LOCAL CLI absolute or ~-prefixed; remote: a path on the hosted machine).\n\n' +
|
|
38
|
+
'DO NOT USE WHEN: a paperclip/attach upload in a different agent (use that agent\'s built-in xlsx skill). Or user-provided files on a remote/hosted deployment (use xlsx_read_handle). Or in-memory bytes the agent already has.',
|
|
42
39
|
inputSchema: {
|
|
43
40
|
type: 'object',
|
|
44
41
|
properties: {
|
|
@@ -110,24 +107,95 @@ const TOOLS = [
|
|
|
110
107
|
{
|
|
111
108
|
name: 'xlsx_write',
|
|
112
109
|
description:
|
|
113
|
-
'create or update a LOCAL .xlsx file from a structured spec.\n' +
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'USE WHEN:
|
|
119
|
-
'Supports multi-sheet workbooks, formulas, named ranges, and table definitions. ' +
|
|
120
|
-
'Server-validated before writing — safer than generating xlsx bytes directly.\n\n' +
|
|
121
|
-
'DO NOT USE WHEN: working in a sandbox without local filesystem write access. ' +
|
|
122
|
-
'Or when the user wants to edit an uploaded file in place (there is no local path to write to).',
|
|
110
|
+
'create or update a LOCAL .xlsx file from a structured spec.\n\n' +
|
|
111
|
+
'Spec shape: `{sheets: [{name, cells: [{address, value | formula}]}]}`. Each cell has an A1 address ("A1", "B2") and EITHER `value` (string|number|boolean|null) OR `formula` (string, no leading "="). Minimal example:\n' +
|
|
112
|
+
'`{"sheets":[{"name":"Sheet1","cells":[{"address":"A1","value":"id"},{"address":"A2","value":1},{"address":"B2","formula":"A2*2"}]}]}`\n\n' +
|
|
113
|
+
'ALWAYS pass out_path to save to disk. Without out_path the workbook bytes return in _meta.file_b64.\n\n' +
|
|
114
|
+
'USE WHEN: the user wants to write or edit a spreadsheet at a LOCAL file path. Server-validated before writing — safer than generating xlsx bytes directly.\n\n' +
|
|
115
|
+
'DO NOT USE WHEN: working in a sandbox without local filesystem write access. Or editing an uploaded file in place (there is no local path to write to).',
|
|
123
116
|
inputSchema: {
|
|
124
117
|
type: 'object',
|
|
125
118
|
properties: {
|
|
126
|
-
spec:
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
spec: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
description:
|
|
122
|
+
'Workbook spec. Shape: {sheets: [{name: string, cells: [{address, value | formula}]}]}. ' +
|
|
123
|
+
'Each cell has an A1-style `address` (regex ^[A-Za-z]+\\d+$) and EXACTLY ONE of `value` ' +
|
|
124
|
+
'(string|number|boolean|null) or `formula` (string WITHOUT leading "=" — e.g. "SUM(A1:A10)" not "=SUM(A1:A10)"). ' +
|
|
125
|
+
'Example: {"sheets":[{"name":"Sheet1","cells":[{"address":"A1","value":"id"},{"address":"A2","value":1},{"address":"B2","formula":"A2*2"}]}]}',
|
|
126
|
+
properties: {
|
|
127
|
+
sheets: {
|
|
128
|
+
type: 'array',
|
|
129
|
+
minItems: 1,
|
|
130
|
+
description: 'One or more sheets. Each sheet is { name: string, cells: array }.',
|
|
131
|
+
items: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
required: ['name', 'cells'],
|
|
134
|
+
properties: {
|
|
135
|
+
name: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
minLength: 1,
|
|
138
|
+
description: 'Sheet name (non-empty).',
|
|
139
|
+
},
|
|
140
|
+
cells: {
|
|
141
|
+
type: 'array',
|
|
142
|
+
description: 'List of cells to write. Order does not matter; addresses are absolute.',
|
|
143
|
+
items: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
required: ['address'],
|
|
146
|
+
description: 'Cell entry. Provide EXACTLY ONE of `value` or `formula`.',
|
|
147
|
+
properties: {
|
|
148
|
+
address: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
pattern: '^[A-Za-z]+\\d+$',
|
|
151
|
+
description: 'A1-style cell address — e.g. "A1", "B2", "AA10".',
|
|
152
|
+
},
|
|
153
|
+
value: {
|
|
154
|
+
type: ['string', 'number', 'boolean', 'null'],
|
|
155
|
+
description: 'Cell value: string, number, boolean, or null. Mutually exclusive with `formula`.',
|
|
156
|
+
},
|
|
157
|
+
formula: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
// No leading `=` — the server expects bare expressions.
|
|
160
|
+
// `^(?!=)` is a negative lookahead that rejects an `=`
|
|
161
|
+
// as the first character; ECMA-262 supported.
|
|
162
|
+
pattern: '^(?!=).+',
|
|
163
|
+
description: 'Excel formula, WITHOUT leading "=". E.g. "SUM(A1:A10)" not "=SUM(A1:A10)". Mutually exclusive with `value`.',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
// Enforce the value-XOR-formula rule at the schema layer
|
|
167
|
+
// so a strict client (or future server) rejects malformed
|
|
168
|
+
// cells before the request fires. SPM 2026-06-06
|
|
169
|
+
// wild-adoption follow-up.
|
|
170
|
+
oneOf: [
|
|
171
|
+
{ required: ['value'], not: { required: ['formula'] } },
|
|
172
|
+
{ required: ['formula'], not: { required: ['value'] } },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
required: ['sheets'],
|
|
181
|
+
},
|
|
182
|
+
spec_path: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'Path to a .json file carrying the spec (alternative to inline spec for large workbooks).',
|
|
185
|
+
},
|
|
186
|
+
out_path: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'Destination .xlsx path. Required when the caller wants the file saved to disk.',
|
|
189
|
+
},
|
|
190
|
+
base_file_b64: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Optional base64 of an existing .xlsx to edit-in-place. When omitted, a fresh workbook is created.',
|
|
193
|
+
},
|
|
129
194
|
},
|
|
130
|
-
|
|
195
|
+
// out_path is the typical caller's choice but not strictly required —
|
|
196
|
+
// when omitted, the workbook bytes return in _meta.file_b64 and the
|
|
197
|
+
// caller saves them (or feeds them to another tool). spec / spec_path
|
|
198
|
+
// is the only hard requirement.
|
|
131
199
|
},
|
|
132
200
|
},
|
|
133
201
|
{
|
|
@@ -1355,7 +1423,63 @@ async function applyFileB64(result, outPath) {
|
|
|
1355
1423
|
// reveal at the boundary.
|
|
1356
1424
|
// ---------------------------------------------------------------------------
|
|
1357
1425
|
|
|
1358
|
-
|
|
1426
|
+
// Defense in depth on the 4xx inline message. The SPEC's bet is that
|
|
1427
|
+
// 4xx server messages describe the CALLER'S OWN INPUT (which field,
|
|
1428
|
+
// what was expected) — but a wrapped 4xx path could still carry
|
|
1429
|
+
// absolute file paths, emails, JWTs / Bearer tokens, Slack tokens,
|
|
1430
|
+
// or other PII. Scrub those before surfacing, replace with `<…>`
|
|
1431
|
+
// placeholders so the caller still sees the SHAPE of the message
|
|
1432
|
+
// without the sensitive payload.
|
|
1433
|
+
//
|
|
1434
|
+
// `<…>` was picked over a more verbose `[redacted-x]` so it's
|
|
1435
|
+
// visually compact and unambiguously not real input.
|
|
1436
|
+
const PII_SCRUBBERS = [
|
|
1437
|
+
// Bearer / Authorization tokens — match before generic JWT pattern.
|
|
1438
|
+
[/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/g, '<bearer>'],
|
|
1439
|
+
// JSON Web Tokens. Three dot-separated base64url segments, the first
|
|
1440
|
+
// starting with `eyJ` (the canonical JWT header prefix).
|
|
1441
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '<jwt>'],
|
|
1442
|
+
// Slack bot / user / app tokens.
|
|
1443
|
+
[/\bxox[bpoars]-[A-Za-z0-9-]{10,}\b/g, '<slack-token>'],
|
|
1444
|
+
// Our own API keys.
|
|
1445
|
+
[/\bxfa_[a-z]+_[A-Za-z0-9]{16,}\b/g, '<xfa-key>'],
|
|
1446
|
+
// Generic 32+ char hex (api keys / hashes).
|
|
1447
|
+
[/\b[a-f0-9]{32,}\b/gi, '<hex>'],
|
|
1448
|
+
// Emails.
|
|
1449
|
+
[/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '<email>'],
|
|
1450
|
+
// POSIX absolute paths under /Users, /home, /var, /opt, /tmp, /etc, /private.
|
|
1451
|
+
[/\/(?:Users|home|var|opt|tmp|etc|private)\/[^\s'"`)\]]+/g, '<path>'],
|
|
1452
|
+
// Windows absolute paths.
|
|
1453
|
+
[/[A-Za-z]:\\[^\s'"`)\]]+/g, '<path>'],
|
|
1454
|
+
];
|
|
1455
|
+
|
|
1456
|
+
// Strip the well-known low-signal noise an inline 4xx surface message
|
|
1457
|
+
// could carry: leading "xlsx-for-ai API error 4xx: " prefix from
|
|
1458
|
+
// lib/client.js, scrub PII via PII_SCRUBBERS, bound the length so a
|
|
1459
|
+
// pathological payload can't blow up the conversation log.
|
|
1460
|
+
const INLINE_4XX_MAX_LEN = 280;
|
|
1461
|
+
function shapeInline4xxMessage(raw) {
|
|
1462
|
+
if (typeof raw !== 'string') return '';
|
|
1463
|
+
let s = raw.replace(/^xlsx-for-ai API error \d+:\s*/i, '').trim();
|
|
1464
|
+
for (const [pattern, replacement] of PII_SCRUBBERS) {
|
|
1465
|
+
s = s.replace(pattern, replacement);
|
|
1466
|
+
}
|
|
1467
|
+
if (s.length > INLINE_4XX_MAX_LEN) {
|
|
1468
|
+
s = s.slice(0, INLINE_4XX_MAX_LEN - 1) + '…';
|
|
1469
|
+
}
|
|
1470
|
+
return s;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function friendlyErrorMessage(toolName, err) {
|
|
1474
|
+
// err may be undefined (defensive) or any thrown value. Extract the
|
|
1475
|
+
// fields we care about safely.
|
|
1476
|
+
const code = err && err.code;
|
|
1477
|
+
const status = err && err.status;
|
|
1478
|
+
const payload = err && err.payload;
|
|
1479
|
+
|
|
1480
|
+
// Known client-side / mcp.js error codes — keep their pre-existing
|
|
1481
|
+
// short text. Ordered before the 4xx default so the specific message
|
|
1482
|
+
// wins.
|
|
1359
1483
|
switch (code) {
|
|
1360
1484
|
case 'DISALLOWED_EXTENSION':
|
|
1361
1485
|
return `${toolName}: file path must point at a workbook (allowed: .xlsx/.xls/.xlsm/.xlsb/.csv/.ods/.fods/.numbers/.tsv).`;
|
|
@@ -1371,8 +1495,6 @@ function friendlyErrorMessage(toolName, code) {
|
|
|
1371
1495
|
return `${toolName}: required token env var is not set (see tool docs for which one).`;
|
|
1372
1496
|
case 'API_UNREACHABLE':
|
|
1373
1497
|
return `${toolName}: API is unreachable — check network connectivity.`;
|
|
1374
|
-
case 'API_SERVER_ERROR':
|
|
1375
|
-
return `${toolName}: API returned a server error — retry shortly.`;
|
|
1376
1498
|
case 'TIER_UPGRADE_REQUIRED':
|
|
1377
1499
|
return `${toolName}: this capability requires a paid tier.`;
|
|
1378
1500
|
case 'RATE_LIMITED':
|
|
@@ -1380,8 +1502,57 @@ function friendlyErrorMessage(toolName, code) {
|
|
|
1380
1502
|
case 'FALLBACK_ENGINE_MISSING':
|
|
1381
1503
|
return `${toolName}: local fallback engine not installed (\`npm install @protobi/exceljs\`).`;
|
|
1382
1504
|
default:
|
|
1383
|
-
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// 4xx client-error class: surface the server's validation message
|
|
1509
|
+
// inline. SPM 2026-06-06 wild-adoption SPEC. The 4xx surface
|
|
1510
|
+
// describes the CALLER'S OWN INPUT shape ("spec.sheets must be an
|
|
1511
|
+
// array", "cells[3].address is not a valid Excel address"); the
|
|
1512
|
+
// caller needs that message to fix their call. 5xx stays generic
|
|
1513
|
+
// (it can carry upstream internals).
|
|
1514
|
+
//
|
|
1515
|
+
// Known specific HTTP statuses are mapped first so they keep their
|
|
1516
|
+
// short curated text:
|
|
1517
|
+
if (code === 'API_CLIENT_ERROR') {
|
|
1518
|
+
if (status === 429) {
|
|
1519
|
+
return `${toolName}: free-tier monthly cap reached — see xlsx-for-ai.dev/pricing.`;
|
|
1520
|
+
}
|
|
1521
|
+
if (status === 402) {
|
|
1522
|
+
return `${toolName}: this capability requires a paid tier.`;
|
|
1523
|
+
}
|
|
1524
|
+
// Generic 4xx: surface the server message. Prefer the structured
|
|
1525
|
+
// shape, fall through to the flat message, fall through to the
|
|
1526
|
+
// wrapped err.message (stripped of the "API error 4xx:" prefix).
|
|
1527
|
+
let inline = '';
|
|
1528
|
+
if (payload && typeof payload === 'object') {
|
|
1529
|
+
const structured = payload.error;
|
|
1530
|
+
if (structured && typeof structured === 'object' && typeof structured.message === 'string') {
|
|
1531
|
+
inline = structured.message;
|
|
1532
|
+
} else if (typeof payload.message === 'string') {
|
|
1533
|
+
inline = payload.message;
|
|
1534
|
+
} else if (typeof payload.error === 'string') {
|
|
1535
|
+
inline = payload.error;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (!inline && err && typeof err.message === 'string') {
|
|
1539
|
+
inline = err.message;
|
|
1540
|
+
}
|
|
1541
|
+
const shaped = shapeInline4xxMessage(inline);
|
|
1542
|
+
if (shaped) {
|
|
1543
|
+
return `${toolName}: ${shaped}`;
|
|
1544
|
+
}
|
|
1545
|
+
// Graceful fallback when no message is available (empty/absent
|
|
1546
|
+
// payload, non-string fields): generic with tool name, no
|
|
1547
|
+
// `undefined`, no `[object Object]`.
|
|
1548
|
+
return `${toolName}: invalid request (no detail provided).`;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// 5xx and everything else — stay generic. Security boundary preserved.
|
|
1552
|
+
if (code === 'API_SERVER_ERROR') {
|
|
1553
|
+
return `${toolName}: API returned a server error — retry shortly.`;
|
|
1384
1554
|
}
|
|
1555
|
+
return `${toolName} failed — see server-side logs (request_id in response _meta) for details.`;
|
|
1385
1556
|
}
|
|
1386
1557
|
|
|
1387
1558
|
// ---------------------------------------------------------------------------
|
|
@@ -1873,8 +2044,7 @@ async function main() {
|
|
|
1873
2044
|
// generic "tool failed" with the tool name so callers can still
|
|
1874
2045
|
// route on it without leaking path/server detail. Pre-Friday-
|
|
1875
2046
|
// external CRITICAL per the Tier-1 audit.
|
|
1876
|
-
const
|
|
1877
|
-
const safeMessage = friendlyErrorMessage(name, code);
|
|
2047
|
+
const safeMessage = friendlyErrorMessage(name, err);
|
|
1878
2048
|
return {
|
|
1879
2049
|
content: [{ type: 'text', text: `xlsx-for-ai error: ${safeMessage}` }],
|
|
1880
2050
|
isError: true,
|
|
@@ -1981,4 +2151,4 @@ if (require.main === module) {
|
|
|
1981
2151
|
// script use TOOLS as the single source of truth for downstream artifacts
|
|
1982
2152
|
// (manifest.json, mcp-tools.json snapshot consumed by the MSFT plugin
|
|
1983
2153
|
// manifest), and to expose helpers under test.
|
|
1984
|
-
module.exports = { applyFileB64, dispatchTool, TOOLS };
|
|
2154
|
+
module.exports = { applyFileB64, dispatchTool, TOOLS, friendlyErrorMessage };
|
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.
|
|
4
|
+
"version": "3.0.11",
|
|
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": {
|