xlsx-for-ai 3.0.15 → 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.
- package/mcp.js +164 -1
- package/package.json +1 -1
package/mcp.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|