xlsx-for-ai 2.25.0 → 2.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +48 -9
- package/mcp.js +95 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -83,8 +83,8 @@ async function runClean(opts, absPath) {
|
|
|
83
83
|
try {
|
|
84
84
|
result = await callTool('xlsx_data_clean', body);
|
|
85
85
|
} catch (err) {
|
|
86
|
-
process.stderr.write(
|
|
87
|
-
process.exit(1);
|
|
86
|
+
process.stderr.write(friendlyCliError('xlsx-for-ai --clean', err) + '\n');
|
|
87
|
+
process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const meta = (result && result._meta) || {};
|
|
@@ -152,6 +152,45 @@ async function runClean(opts, absPath) {
|
|
|
152
152
|
|
|
153
153
|
const STAMP_SUBCOMMANDS = new Set(['stamp', 'verify-stamp', 'receipt', 'verify-receipt']);
|
|
154
154
|
|
|
155
|
+
// Strip _meta.file_b64 before writing the meta block to stdout. The
|
|
156
|
+
// stamped/receipted workbook can be megabytes; dumping it to a terminal
|
|
157
|
+
// or CI log clobbers scrollback AND leaks PII-bearing workbook contents
|
|
158
|
+
// to whatever consumes stdout. The file is already saved to disk via
|
|
159
|
+
// the sidecar / --out path; the b64 in stdout serves no consumer.
|
|
160
|
+
// Pre-Friday-external CRITICAL per the Tier-1 audit.
|
|
161
|
+
function metaForStdout(meta) {
|
|
162
|
+
if (!meta || typeof meta !== 'object') return meta;
|
|
163
|
+
const out = { ...meta };
|
|
164
|
+
delete out.file_b64;
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// CLI-side error formatter. Same posture as friendlyErrorMessage in
|
|
169
|
+
// mcp.js: known operational codes get short, client-safe text; everything
|
|
170
|
+
// else collapses to a generic message. err.message can carry absolute
|
|
171
|
+
// file paths, upstream stack traces, and third-party HTTP response
|
|
172
|
+
// bodies — none of those belong in user-facing CLI stderr or in CI logs.
|
|
173
|
+
// Set XFA_DEBUG=1 to see the raw underlying message (for incident triage).
|
|
174
|
+
function friendlyCliError(prefix, err) {
|
|
175
|
+
const code = err && err.code;
|
|
176
|
+
const showRaw = process.env.XFA_DEBUG === '1';
|
|
177
|
+
const base = (() => {
|
|
178
|
+
switch (code) {
|
|
179
|
+
case 'API_UNREACHABLE': return `${prefix}: API is unreachable — check network connectivity.`;
|
|
180
|
+
case 'API_SERVER_ERROR': return `${prefix}: API returned a server error — retry shortly.`;
|
|
181
|
+
case 'DISALLOWED_EXTENSION': return `${prefix}: file must be a workbook (allowed: .xlsx/.xls/.xlsm/.xlsb/.csv/.ods/.fods/.numbers/.tsv).`;
|
|
182
|
+
case 'FILE_TOO_LARGE': return `${prefix}: file exceeds the XFA_MAX_FILE_MB cap (default 50 MB).`;
|
|
183
|
+
case 'FILE_NOT_FOUND': return `${prefix}: file not found.`;
|
|
184
|
+
case 'MISSING_TOKEN': return `${prefix}: required token env var is not set.`;
|
|
185
|
+
case 'RATE_LIMITED': return `${prefix}: free-tier monthly cap reached — see xlsx-for-ai.dev/pricing.`;
|
|
186
|
+
case 'TIER_UPGRADE_REQUIRED': return `${prefix}: this capability requires a paid tier.`;
|
|
187
|
+
case 'FALLBACK_ENGINE_MISSING': return `${prefix}: local fallback engine not installed (\`npm install @protobi/exceljs\`).`;
|
|
188
|
+
default: return `${prefix}: request failed${code ? ` (code=${code})` : ''}.`;
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
return showRaw && err && err.message ? `${base}\nRaw: ${err.message}` : base;
|
|
192
|
+
}
|
|
193
|
+
|
|
155
194
|
function nextRequiredArg(argv, i, flag) {
|
|
156
195
|
const v = argv[i + 1];
|
|
157
196
|
if (v === undefined || v.startsWith('-')) {
|
|
@@ -204,7 +243,7 @@ async function runStampSubcommand(subcmd, rest) {
|
|
|
204
243
|
if (excludeSheets.length) body.exclude_sheets = excludeSheets;
|
|
205
244
|
if (supervisor) body.generated_by = { npm: 'xlsx-for-ai@' + require('./package.json').version, supervisor };
|
|
206
245
|
const result = await callServerForStamp('xlsx_stamp', body, outPath, filePath, '.stamped.xlsx');
|
|
207
|
-
process.stdout.write(JSON.stringify(result._meta || {}, null, 2) + '\n');
|
|
246
|
+
process.stdout.write(JSON.stringify(metaForStdout(result._meta) || {}, null, 2) + '\n');
|
|
208
247
|
return 0;
|
|
209
248
|
}
|
|
210
249
|
|
|
@@ -212,7 +251,7 @@ async function runStampSubcommand(subcmd, rest) {
|
|
|
212
251
|
const body = { file_b64: fileB64 };
|
|
213
252
|
const result = await callTool('xlsx_verify_stamp', body);
|
|
214
253
|
const meta = result._meta || {};
|
|
215
|
-
process.stdout.write(JSON.stringify(meta, null, 2) + '\n');
|
|
254
|
+
process.stdout.write(JSON.stringify(metaForStdout(meta), null, 2) + '\n');
|
|
216
255
|
return meta.valid === true ? 0 : 1;
|
|
217
256
|
}
|
|
218
257
|
|
|
@@ -255,7 +294,7 @@ async function runStampSubcommand(subcmd, rest) {
|
|
|
255
294
|
if (description) body.description = description;
|
|
256
295
|
if (coverSheets.length) body.covers_sheets = coverSheets;
|
|
257
296
|
const result = await callServerForStamp('xlsx_receipt', body, outPath, filePath, '.receipted.xlsx');
|
|
258
|
-
process.stdout.write(JSON.stringify(result._meta || {}, null, 2) + '\n');
|
|
297
|
+
process.stdout.write(JSON.stringify(metaForStdout(result._meta) || {}, null, 2) + '\n');
|
|
259
298
|
return 0;
|
|
260
299
|
}
|
|
261
300
|
|
|
@@ -263,7 +302,7 @@ async function runStampSubcommand(subcmd, rest) {
|
|
|
263
302
|
const body = { file_b64: fileB64 };
|
|
264
303
|
const result = await callTool('xlsx_verify_receipt', body);
|
|
265
304
|
const meta = result._meta || {};
|
|
266
|
-
process.stdout.write(JSON.stringify(meta, null, 2) + '\n');
|
|
305
|
+
process.stdout.write(JSON.stringify(metaForStdout(meta), null, 2) + '\n');
|
|
267
306
|
return meta.valid === true ? 0 : 1;
|
|
268
307
|
}
|
|
269
308
|
return 2;
|
|
@@ -274,7 +313,7 @@ async function callServerForStamp(tool, body, explicitOutPath, sourcePath, sidec
|
|
|
274
313
|
try {
|
|
275
314
|
result = await callTool(tool, body);
|
|
276
315
|
} catch (err) {
|
|
277
|
-
process.stderr.write(`xlsx-for-ai ${tool}
|
|
316
|
+
process.stderr.write(friendlyCliError(`xlsx-for-ai ${tool}`, err) + '\n');
|
|
278
317
|
process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
|
|
279
318
|
}
|
|
280
319
|
const meta = result._meta || {};
|
|
@@ -353,7 +392,7 @@ async function main() {
|
|
|
353
392
|
if (err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR') {
|
|
354
393
|
result = await fallbackRead(absPath, opts);
|
|
355
394
|
} else {
|
|
356
|
-
process.stderr.write(
|
|
395
|
+
process.stderr.write(friendlyCliError('xlsx-for-ai', err) + '\n');
|
|
357
396
|
process.exit(1);
|
|
358
397
|
}
|
|
359
398
|
}
|
|
@@ -363,6 +402,6 @@ async function main() {
|
|
|
363
402
|
}
|
|
364
403
|
|
|
365
404
|
main().catch((err) => {
|
|
366
|
-
process.stderr.write(
|
|
405
|
+
process.stderr.write(friendlyCliError('xlsx-for-ai', err) + '\n');
|
|
367
406
|
process.exit(1);
|
|
368
407
|
});
|
package/mcp.js
CHANGED
|
@@ -1156,6 +1156,14 @@ function fileToB64(filePath) {
|
|
|
1156
1156
|
// If out_path is not provided: leave response unchanged.
|
|
1157
1157
|
// ---------------------------------------------------------------------------
|
|
1158
1158
|
|
|
1159
|
+
// Extensions an MCP tool is allowed to write via out_path. Tighter than the
|
|
1160
|
+
// READ allowlist (no .ods/.fods/.numbers/.tsv) because the server only ever
|
|
1161
|
+
// emits XLSX or XLSX-family workbook bytes — accepting unrelated extensions
|
|
1162
|
+
// would let a malicious / confused agent point out_path at /etc/profile.d/
|
|
1163
|
+
// or a shell startup script. The .json carve-out is for fixture/audit JSON
|
|
1164
|
+
// the redact + clean tools sometimes emit alongside the workbook.
|
|
1165
|
+
const ALLOWED_WRITE_EXTENSIONS = new Set(['.xlsx', '.xls', '.xlsm', '.xlsb', '.csv', '.json']);
|
|
1166
|
+
|
|
1159
1167
|
async function applyFileB64(result, outPath) {
|
|
1160
1168
|
if (!outPath) {
|
|
1161
1169
|
// No save requested — leave response untouched (b64 stays in _meta for caller)
|
|
@@ -1164,6 +1172,21 @@ async function applyFileB64(result, outPath) {
|
|
|
1164
1172
|
|
|
1165
1173
|
const absPath = path.resolve(outPath);
|
|
1166
1174
|
|
|
1175
|
+
// Containment: require an absolute path + a workbook-family extension.
|
|
1176
|
+
// Reject path-traversal patterns and any non-workbook extension at the
|
|
1177
|
+
// boundary so a malicious agent can't request a write to a shell-startup
|
|
1178
|
+
// location or an arbitrary system file via out_path. Pre-Friday-external
|
|
1179
|
+
// CRITICAL per the Tier-1 error-handling audit (2026-06-03).
|
|
1180
|
+
const outExt = path.extname(absPath).toLowerCase();
|
|
1181
|
+
if (!ALLOWED_WRITE_EXTENSIONS.has(outExt)) {
|
|
1182
|
+
if (result.content && result.content[0] && result.content[0].type === 'text') {
|
|
1183
|
+
result.content[0].text +=
|
|
1184
|
+
`\n\nout_path rejected: extension "${outExt}" is not in the allowed write set ` +
|
|
1185
|
+
`(${[...ALLOWED_WRITE_EXTENSIONS].join(', ')}). File was NOT written.`;
|
|
1186
|
+
}
|
|
1187
|
+
return result;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1167
1190
|
if (result._meta && result._meta.file_b64) {
|
|
1168
1191
|
await fsPromises.writeFile(absPath, Buffer.from(result._meta.file_b64, 'base64'));
|
|
1169
1192
|
// Append save confirmation to first text content block
|
|
@@ -1181,6 +1204,51 @@ async function applyFileB64(result, outPath) {
|
|
|
1181
1204
|
return result;
|
|
1182
1205
|
}
|
|
1183
1206
|
|
|
1207
|
+
// ---------------------------------------------------------------------------
|
|
1208
|
+
// Boundary error sanitization
|
|
1209
|
+
//
|
|
1210
|
+
// The MCP server is a public boundary — anything in err.message that flows
|
|
1211
|
+
// to the client can end up in the MCP client's conversation log and from
|
|
1212
|
+
// there into any LLM context window the operator never intended. Map the
|
|
1213
|
+
// known operational error codes to short, client-safe text; collapse
|
|
1214
|
+
// everything else to a generic message that names the tool but not the
|
|
1215
|
+
// internals. Tool name is safe to echo (the caller asked for it); paths,
|
|
1216
|
+
// upstream server stacks, and third-party response bodies are not.
|
|
1217
|
+
//
|
|
1218
|
+
// New codes added here as the client-side error surface grows. Default
|
|
1219
|
+
// branch is conservative on purpose — better to under-reveal than over-
|
|
1220
|
+
// reveal at the boundary.
|
|
1221
|
+
// ---------------------------------------------------------------------------
|
|
1222
|
+
|
|
1223
|
+
function friendlyErrorMessage(toolName, code) {
|
|
1224
|
+
switch (code) {
|
|
1225
|
+
case 'DISALLOWED_EXTENSION':
|
|
1226
|
+
return `${toolName}: file path must point at a workbook (allowed: .xlsx/.xls/.xlsm/.xlsb/.csv/.ods/.fods/.numbers/.tsv).`;
|
|
1227
|
+
case 'SYMLINK_REJECTED':
|
|
1228
|
+
return `${toolName}: file path resolves through a symlink — provide a direct path.`;
|
|
1229
|
+
case 'FILE_TOO_LARGE':
|
|
1230
|
+
return `${toolName}: file exceeds the XFA_MAX_FILE_MB cap (default 50 MB).`;
|
|
1231
|
+
case 'FILE_NOT_FOUND':
|
|
1232
|
+
return `${toolName}: file not found at the supplied path.`;
|
|
1233
|
+
case 'NOT_REGULAR_FILE':
|
|
1234
|
+
return `${toolName}: file path is not a regular file.`;
|
|
1235
|
+
case 'MISSING_TOKEN':
|
|
1236
|
+
return `${toolName}: required token env var is not set (see tool docs for which one).`;
|
|
1237
|
+
case 'API_UNREACHABLE':
|
|
1238
|
+
return `${toolName}: API is unreachable — check network connectivity.`;
|
|
1239
|
+
case 'API_SERVER_ERROR':
|
|
1240
|
+
return `${toolName}: API returned a server error — retry shortly.`;
|
|
1241
|
+
case 'TIER_UPGRADE_REQUIRED':
|
|
1242
|
+
return `${toolName}: this capability requires a paid tier.`;
|
|
1243
|
+
case 'RATE_LIMITED':
|
|
1244
|
+
return `${toolName}: free-tier monthly cap reached — see xlsx-for-ai.dev/pricing.`;
|
|
1245
|
+
case 'FALLBACK_ENGINE_MISSING':
|
|
1246
|
+
return `${toolName}: local fallback engine not installed (\`npm install @protobi/exceljs\`).`;
|
|
1247
|
+
default:
|
|
1248
|
+
return `${toolName} failed — see server-side logs (request_id in response _meta) for details.`;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1184
1252
|
// ---------------------------------------------------------------------------
|
|
1185
1253
|
// Tool dispatch
|
|
1186
1254
|
// ---------------------------------------------------------------------------
|
|
@@ -1525,9 +1593,23 @@ async function main() {
|
|
|
1525
1593
|
// server-side tools appear without re-publishing this npm package.
|
|
1526
1594
|
// resolveCatalog returns the baked-in TOOLS as last-resort fallback so
|
|
1527
1595
|
// we never fail-open on a transient network blip. See lib/discover.js.
|
|
1596
|
+
//
|
|
1597
|
+
// Hard timeout (8s) on top of any timeout inside resolveCatalog so that
|
|
1598
|
+
// a network call which hangs forever (DNS sinkhole, TCP black hole, slow-
|
|
1599
|
+
// loris-style stalled response) cannot block MCP server startup
|
|
1600
|
+
// indefinitely. Pre-Friday-external CRITICAL per the Tier-1 audit.
|
|
1601
|
+
const CATALOG_FETCH_TIMEOUT_MS = 8000;
|
|
1528
1602
|
let catalog;
|
|
1529
1603
|
try {
|
|
1530
|
-
catalog = await
|
|
1604
|
+
catalog = await Promise.race([
|
|
1605
|
+
resolveCatalog(TOOLS),
|
|
1606
|
+
new Promise((_, reject) =>
|
|
1607
|
+
setTimeout(
|
|
1608
|
+
() => reject(new Error(`catalog fetch timed out after ${CATALOG_FETCH_TIMEOUT_MS}ms`)),
|
|
1609
|
+
CATALOG_FETCH_TIMEOUT_MS
|
|
1610
|
+
)
|
|
1611
|
+
),
|
|
1612
|
+
]);
|
|
1531
1613
|
} catch (_) {
|
|
1532
1614
|
catalog = { tools: TOOLS, source: 'static-fallback' };
|
|
1533
1615
|
}
|
|
@@ -1568,8 +1650,19 @@ async function main() {
|
|
|
1568
1650
|
// Pass API response through verbatim (citation footer + _meta preserved)
|
|
1569
1651
|
return result;
|
|
1570
1652
|
} catch (err) {
|
|
1653
|
+
// Error message sanitization at the MCP boundary. Raw err.message
|
|
1654
|
+
// can leak absolute file paths (FILE_NOT_FOUND), upstream server
|
|
1655
|
+
// error stacks (any thrown Error inside dispatchTool), and upstream
|
|
1656
|
+
// HTTP response bodies (callTool's API_SERVER_ERROR path may carry
|
|
1657
|
+
// server internals). Translate the known operational codes into
|
|
1658
|
+
// short, client-safe messages; everything else collapses to a
|
|
1659
|
+
// generic "tool failed" with the tool name so callers can still
|
|
1660
|
+
// route on it without leaking path/server detail. Pre-Friday-
|
|
1661
|
+
// external CRITICAL per the Tier-1 audit.
|
|
1662
|
+
const code = err && err.code;
|
|
1663
|
+
const safeMessage = friendlyErrorMessage(name, code);
|
|
1571
1664
|
return {
|
|
1572
|
-
content: [{ type: 'text', text: `xlsx-for-ai error: ${
|
|
1665
|
+
content: [{ type: 'text', text: `xlsx-for-ai error: ${safeMessage}` }],
|
|
1573
1666
|
isError: true,
|
|
1574
1667
|
};
|
|
1575
1668
|
}
|
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.25.
|
|
4
|
+
"version": "2.25.2",
|
|
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": {
|