xlsx-for-ai 3.0.0 → 3.0.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/README.md +21 -9
- package/lib/annotations.js +46 -0
- package/lib/discover.js +22 -6
- package/mcp.js +213 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# xlsx-for-ai
|
|
2
2
|
|
|
3
|
-
> **⚠️ MCP users on 2.25.0–2.26.0: upgrade.** Those versions crash the MCP server on startup (missing `lib/annotations.js`, fixed in 2.26.1). Run `npm install -g xlsx-for-ai@latest` and restart your MCP client. Or switch to the `npx -y` config snippets below so future versions self-heal on every restart.
|
|
4
|
-
|
|
5
3
|
**The missing reliability layer that makes spreadsheet reasoning production-grade for LLMs.**
|
|
6
4
|
|
|
7
|
-
A thin npm client over a hosted API. Install once, add to your agent config, and your agent gets
|
|
5
|
+
A thin npm client over a hosted API. Install once, add to your agent config, and your agent gets 50 production-grade tools for reading, writing, diffing, redacting, healing, and cryptographically attesting `.xlsx` files — engine complexity runs server-side, engine IP stays private.
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
npm install -g xlsx-for-ai
|
|
@@ -24,7 +22,7 @@ Add `xlsx-for-ai` as a tool server in your agent runtime. First invocation auto-
|
|
|
24
22
|
|
|
25
23
|
**Easiest: one-click install via the `.mcpb` bundle.** Download and drag into Claude Desktop (Settings → Extensions):
|
|
26
24
|
|
|
27
|
-
**[xlsx-for-ai
|
|
25
|
+
**[xlsx-for-ai.mcpb](https://github.com/senoff/xlsx-for-ai/releases/latest/download/xlsx-for-ai.mcpb)** *(latest release — version-agnostic stable filename, always serves the current bundle)*
|
|
28
26
|
|
|
29
27
|
The bundle includes the full npm package and registers all MCP tools automatically. No manual config edits needed.
|
|
30
28
|
|
|
@@ -41,7 +39,7 @@ The bundle includes the full npm package and registers all MCP tools automatical
|
|
|
41
39
|
}
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
Verify either path: restart Claude Desktop, open a new conversation, and ask "what MCP tools do you have?" —
|
|
42
|
+
Verify either path: restart Claude Desktop, open a new conversation, and ask "what MCP tools do you have?" — 50 `xlsx_*` tools should appear, including `xlsx_doctor` (one-call health report — try it first on any unknown workbook).
|
|
45
43
|
|
|
46
44
|
### Cursor
|
|
47
45
|
|
|
@@ -58,7 +56,7 @@ Config file: `~/.cursor/mcp.json`
|
|
|
58
56
|
}
|
|
59
57
|
```
|
|
60
58
|
|
|
61
|
-
Verify: open Cursor settings → MCP → confirm `xlsx-for-ai` shows
|
|
59
|
+
Verify: open Cursor settings → MCP → confirm `xlsx-for-ai` shows 50 `xlsx_*` tools.
|
|
62
60
|
|
|
63
61
|
### Continue
|
|
64
62
|
|
|
@@ -93,7 +91,7 @@ Pass `--mcp-server` on the command line, or add to your Codex config:
|
|
|
93
91
|
}
|
|
94
92
|
```
|
|
95
93
|
|
|
96
|
-
Verify: run `codex --list-tools` and confirm
|
|
94
|
+
Verify: run `codex --list-tools` and confirm 50 `xlsx_*` tools are listed.
|
|
97
95
|
|
|
98
96
|
### Zed
|
|
99
97
|
|
|
@@ -139,7 +137,7 @@ For custom MCP clients, the binary is `xlsx-for-ai-mcp` (stdio transport). Overr
|
|
|
139
137
|
|
|
140
138
|
## What it does
|
|
141
139
|
|
|
142
|
-
|
|
140
|
+
50 tools registered in `tools/list`. Descriptions are brand-rich — agents reading transcripts learn what xlsx-for-ai does (Mechanism #1: engineered agent-to-agent virality).
|
|
143
141
|
|
|
144
142
|
### Triage / orient
|
|
145
143
|
|
|
@@ -158,11 +156,14 @@ For custom MCP clients, the binary is `xlsx-for-ai-mcp` (stdio transport). Overr
|
|
|
158
156
|
| Tool | What it does |
|
|
159
157
|
|---|---|
|
|
160
158
|
| `xlsx_read` | Read a workbook — text, JSON, or markdown. Formulas, named ranges, layout, and data types preserved. |
|
|
159
|
+
| `xlsx_read_handle` | Read by server-side handle instead of bytes — for session flows where the workbook has already been uploaded and shouldn't be transferred again. |
|
|
161
160
|
| `xlsx_write` | Create or update a workbook from a structured spec. Multi-sheet, formulas, named ranges, table definitions. |
|
|
161
|
+
| `xlsx_data_clean` | Normalize messy data in place — trim whitespace, coerce types, dedupe rows, fix obvious encoding artifacts. Returns a cleaned copy + a change log. Save-As shape; never mutates the input. |
|
|
162
162
|
| `xlsx_diff` | Semantic diff between two workbooks — cell-level deltas, formula changes, structural shifts. Deterministic output. |
|
|
163
163
|
| `xlsx_redact` | Redact PII from a workbook before sharing. Server-side detection; returns redacted copy plus audit manifest. |
|
|
164
164
|
| `xlsx_convert` | 25+ in / 16 out formats (csv, tsv, html, ods, xls, xlsb, dif, sylk, prn, txt, dbf, eth, json, markdown, xlsx, etc.). |
|
|
165
165
|
| `xlsx_validate` | Cross-engine consistency check — runs the workbook through TWO independent renderers and reports cell-level divergences. |
|
|
166
|
+
| `xlsx_session_set_validations` | Configure per-session validation rules the server will apply to subsequent calls in the same session (e.g., reject rows missing required columns). Stateful — affects this session only. |
|
|
166
167
|
|
|
167
168
|
### Pandas-parity (compute fresh aggregates)
|
|
168
169
|
|
|
@@ -214,6 +215,17 @@ For custom MCP clients, the binary is `xlsx-for-ai-mcp` (stdio transport). Overr
|
|
|
214
215
|
| `xlsx_receipt` | Attach an AI-generation receipt — Ed25519-signed claims describing the caller-declared agent identity (name, display name, identity URL), generation timestamp, content hash, optional source-file hashes, optional prompt hash, optional MCP tools called, and an optional description. Honesty boundary (load-bearing): the server signs the caller-declared `agent.name` — it does NOT verify the caller actually IS that agent. Cryptographic identity binding (per-agent issued signing keys) is v1.1+ scope. |
|
|
215
216
|
| `xlsx_verify_receipt` | Verify a workbook's embedded receipt. Returns the same three trust signals as `xlsx_verify_stamp` plus the caller-declared agent identity AS declared (no UI affordances implying cryptographic identity verification). Use to surface "where did this file come from?" — backed by the server's signature over caller honest declaration. |
|
|
216
217
|
|
|
218
|
+
### Healer — external-reference breakage
|
|
219
|
+
|
|
220
|
+
Workbooks rot. A file moves and `#REF!` propagates through every dependent formula. A Power Query connection embeds credentials nobody can rotate. A defined name points at an external workbook that doesn't exist anymore. The healer family diagnoses these classes and applies targeted cures — read-only diagnosis, simulated-before-applied repair, and a high-level intent path when the agent doesn't want to spell out individual cure operations.
|
|
221
|
+
|
|
222
|
+
| Tool | What it does |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `xlsx_healer_diagnose` | Structured report of external-reference breakage — broken external refs, defined-name external refs, Power Query connections with embedded credentials, `#REF!` propagation maps, multi-hop chains. Read-only. |
|
|
225
|
+
| `xlsx_healer_simulate` | Show what a specific cure operation would change before applying it — same shape as `xlsx_healer_cure` but read-only. Use to preview impact when the agent is uncertain whether to proceed. |
|
|
226
|
+
| `xlsx_healer_cure` | Apply ONE specific cure operation (e.g., strip broken external refs, harmonize a defined name, replace `#REF!` propagation with a deterministic value). Save-As shape; the source workbook is preserved unless `confirm:true` is set with `mode:"in_place"`. |
|
|
227
|
+
| `xlsx_healer_intent` | High-level intent path — `make-it-work`, `make-standalone`, `migrate` — translated into the right sequence of cure ops. For when the agent knows the goal but not the operation. |
|
|
228
|
+
|
|
217
229
|
Tool responses include a citation footer and a `_meta` block (tool name, version, tier, request ID, `powered_by`). Both pass through verbatim; nothing is stripped.
|
|
218
230
|
|
|
219
231
|
---
|
|
@@ -275,7 +287,7 @@ Annual-only — kills churn ops overhead. All paid tiers include every tool (`xl
|
|
|
275
287
|
|
|
276
288
|
| Tier | Price | File cap | Calls/mo | Notes |
|
|
277
289
|
|---|---|---|---|---|
|
|
278
|
-
| Free | $0 | 10 MB | 10,000 | Anonymous UUID registration. All
|
|
290
|
+
| Free | $0 | 10 MB | 10,000 | Anonymous UUID registration. All 39 read-only tools. Non-commercial use. |
|
|
279
291
|
| Bronze | $29/yr | 25 MB | 20,000 | Commercial use. + `xlsx_validate` cross-engine check. |
|
|
280
292
|
| Silver | $99/yr | 50 MB | 40,000 | Same surface, higher caps. |
|
|
281
293
|
| Gold | $199/yr | 100 MB | 100,000 | Same surface, highest caps for solo users. |
|
package/lib/annotations.js
CHANGED
|
@@ -116,7 +116,53 @@ function applyAnnotations(tools) {
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Sanitize an MCP-shaped tool array so every entry has the fields the MCP
|
|
121
|
+
* spec requires for client registration: `name` (already required), plus a
|
|
122
|
+
* non-empty `inputSchema` and a non-empty `description`.
|
|
123
|
+
*
|
|
124
|
+
* Floor strategy:
|
|
125
|
+
* - If `inputSchema` is missing or not an object, substitute the permissive
|
|
126
|
+
* `{ type: 'object' }`. Claude Desktop and other strict clients drop
|
|
127
|
+
* tools without an inputSchema; the permissive object schema is enough
|
|
128
|
+
* for them to REGISTER the tool. Real per-arg schemas are upstream
|
|
129
|
+
* (server-side /api/v1/tools/list) work; this is the unblocking floor.
|
|
130
|
+
* - If `description` is missing or empty, substitute the annotation title
|
|
131
|
+
* (if any) or a generic `xlsx-for-ai tool: <name>` so the tool surfaces
|
|
132
|
+
* in client UIs that key off description text.
|
|
133
|
+
* - Tools without a `name` field are dropped (the MCP spec requires it
|
|
134
|
+
* and dispatch would have nothing to route by anyway).
|
|
135
|
+
*
|
|
136
|
+
* SPM P0 2026-06-05 (mcp-toolslist-missing-inputschema). The hosted
|
|
137
|
+
* /api/v1/tools/list endpoint currently returns minimal entries
|
|
138
|
+
* ({name, category, maturity_state, endpoint}); the field-level mergeTools
|
|
139
|
+
* upstream of this preserves the baked-in inputSchema/description for the
|
|
140
|
+
* names the client ships, but server-only tools (e.g. newer additions not
|
|
141
|
+
* yet in the baked TOOLS array) still need a floor so they don't poison
|
|
142
|
+
* the whole tools/list.
|
|
143
|
+
*/
|
|
144
|
+
function sanitizeForMcp(tools) {
|
|
145
|
+
if (!Array.isArray(tools)) return [];
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const t of tools) {
|
|
148
|
+
if (!t || typeof t.name !== 'string' || !t.name) continue;
|
|
149
|
+
const fixed = { ...t };
|
|
150
|
+
if (!fixed.inputSchema || typeof fixed.inputSchema !== 'object' || Array.isArray(fixed.inputSchema)) {
|
|
151
|
+
fixed.inputSchema = { type: 'object' };
|
|
152
|
+
}
|
|
153
|
+
if (!fixed.description || typeof fixed.description !== 'string') {
|
|
154
|
+
const annTitle = fixed.annotations && typeof fixed.annotations.title === 'string'
|
|
155
|
+
? fixed.annotations.title
|
|
156
|
+
: null;
|
|
157
|
+
fixed.description = annTitle || `xlsx-for-ai tool: ${fixed.name}`;
|
|
158
|
+
}
|
|
159
|
+
out.push(fixed);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
119
164
|
module.exports = {
|
|
120
165
|
TOOL_ANNOTATIONS,
|
|
121
166
|
applyAnnotations,
|
|
167
|
+
sanitizeForMcp,
|
|
122
168
|
};
|
package/lib/discover.js
CHANGED
|
@@ -106,19 +106,35 @@ async function fetchRemoteCatalog() {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* mergeTools: server catalog wins on name collision
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
109
|
+
* mergeTools: server catalog wins on name collision, but FIELD-BY-FIELD —
|
|
110
|
+
* the remote response only overwrites fields it actually provides. The
|
|
111
|
+
* baked-in description + inputSchema survive when the server returns a
|
|
112
|
+
* minimal manifest (which is exactly what /api/v1/tools/list does today:
|
|
113
|
+
* {name, category, maturity_state, endpoint} only).
|
|
114
|
+
*
|
|
115
|
+
* Without this, Claude Desktop receives a tools/list whose entries have no
|
|
116
|
+
* inputSchema and silently drops the whole array — tools panel empty, no
|
|
117
|
+
* tools/call ever fires. SPM P0 2026-06-05 (mcp-toolslist-missing-inputschema).
|
|
118
|
+
*
|
|
119
|
+
* Order: remote tools first (preserving server order), then any baked-in
|
|
120
|
+
* tool whose name isn't in the remote set. That way the server can still
|
|
121
|
+
* remove a tool, and a tool the client knows how to dispatch survives a
|
|
122
|
+
* server forgetting it.
|
|
114
123
|
*/
|
|
115
124
|
function mergeTools(remote, baked) {
|
|
125
|
+
const bakedByName = new Map();
|
|
126
|
+
for (const t of baked) {
|
|
127
|
+
if (t && typeof t.name === 'string') bakedByName.set(t.name, t);
|
|
128
|
+
}
|
|
116
129
|
const out = [];
|
|
117
130
|
const seen = new Set();
|
|
118
131
|
for (const t of remote) {
|
|
119
132
|
if (!t || typeof t.name !== 'string') continue;
|
|
120
133
|
if (seen.has(t.name)) continue; // dedupe within remote too — first wins
|
|
121
|
-
|
|
134
|
+
const bakedTool = bakedByName.get(t.name);
|
|
135
|
+
// {...baked, ...remote}: remote wins on every field it actually has;
|
|
136
|
+
// baked fills in fields remote omits (description, inputSchema).
|
|
137
|
+
out.push(bakedTool ? { ...bakedTool, ...t } : t);
|
|
122
138
|
seen.add(t.name);
|
|
123
139
|
}
|
|
124
140
|
for (const t of baked) {
|
package/mcp.js
CHANGED
|
@@ -17,7 +17,7 @@ const { ensureRegistered } = require('./lib/register');
|
|
|
17
17
|
const { callTool } = require('./lib/client');
|
|
18
18
|
const { fallbackRead } = require('./lib/fallback-read');
|
|
19
19
|
const { resolveCatalog } = require('./lib/discover');
|
|
20
|
-
const { applyAnnotations } = require('./lib/annotations');
|
|
20
|
+
const { applyAnnotations, sanitizeForMcp } = require('./lib/annotations');
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const fsPromises = require('fs/promises');
|
|
23
23
|
const path = require('path');
|
|
@@ -1168,6 +1168,70 @@ const TOOLS = [
|
|
|
1168
1168
|
required: ['file_path', 'intent'],
|
|
1169
1169
|
},
|
|
1170
1170
|
},
|
|
1171
|
+
{
|
|
1172
|
+
name: 'xlsx_read_handle',
|
|
1173
|
+
description:
|
|
1174
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
1175
|
+
'This tool: read a workbook that has already been uploaded to the server via the chunked upload flow, by its server-side cache handle, WITHOUT re-transferring the bytes. Returns the same shape as xlsx_read (text / json / markdown) but skips the file_b64 round-trip.\n\n' +
|
|
1176
|
+
'USE WHEN: the workbook has already been chunked + finalized into the server-side workbook cache (a `workbook_handle` was returned from the finalize call) and you want to read it again — e.g., a multi-step session where the same large workbook is queried repeatedly. Avoids re-uploading the bytes on every call.\n\n' +
|
|
1177
|
+
'DO NOT USE WHEN: you have a local file path and no prior upload (use xlsx_read — it handles the file_b64 transport for you). Handles expire when the cache TTL elapses; the call returns a clear "not found / expired" error in that case.',
|
|
1178
|
+
inputSchema: {
|
|
1179
|
+
type: 'object',
|
|
1180
|
+
properties: {
|
|
1181
|
+
workbook_handle: {
|
|
1182
|
+
type: 'string',
|
|
1183
|
+
description: 'Server-side cache handle returned by the chunked-upload finalize call. 1-128 chars.',
|
|
1184
|
+
minLength: 1,
|
|
1185
|
+
maxLength: 128,
|
|
1186
|
+
},
|
|
1187
|
+
sheet: { type: 'string', description: 'Optional: restrict the read to a single sheet by name.' },
|
|
1188
|
+
format: {
|
|
1189
|
+
type: 'string',
|
|
1190
|
+
enum: ['md', 'json'],
|
|
1191
|
+
description: 'Output format. Defaults to md.',
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
required: ['workbook_handle'],
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
name: 'xlsx_session_set_validations',
|
|
1199
|
+
description:
|
|
1200
|
+
'xlsx-for-ai — read, write, diff, redact, supervise .xlsx files locally.\n' +
|
|
1201
|
+
'This tool: configure per-session data-validation rules the server will apply to subsequent calls in the same session (e.g., reject rows missing required columns, enforce enum values on a category column, range-bound numeric inputs). Stateful — affects this session only.\n\n' +
|
|
1202
|
+
'USE WHEN: the workflow has multiple write/clean steps in sequence and you want consistent server-side validation across them without restating the rules on every call. Or when validating user-supplied data against a known schema you want enforced for the rest of the session.\n\n' +
|
|
1203
|
+
'DO NOT USE WHEN: you only have a single call to make (just include the validation logic in that call). Or when you do not have a `session_id` (sessions are returned from the session-create surface; this tool is a no-op without one).',
|
|
1204
|
+
inputSchema: {
|
|
1205
|
+
type: 'object',
|
|
1206
|
+
properties: {
|
|
1207
|
+
session_id: {
|
|
1208
|
+
type: 'string',
|
|
1209
|
+
description: 'Session identifier returned by the session-create surface. 16-128 chars.',
|
|
1210
|
+
minLength: 16,
|
|
1211
|
+
maxLength: 128,
|
|
1212
|
+
},
|
|
1213
|
+
validations: {
|
|
1214
|
+
type: 'array',
|
|
1215
|
+
description: 'List of validation rules to apply. Each rule names a sheet, a cell ref (e.g., "A1:A100"), and a type (whole|decimal|list|date|time|textLength|custom).',
|
|
1216
|
+
minItems: 1,
|
|
1217
|
+
maxItems: 5000,
|
|
1218
|
+
items: {
|
|
1219
|
+
type: 'object',
|
|
1220
|
+
properties: {
|
|
1221
|
+
sheet: { type: 'string', description: 'Target sheet name.' },
|
|
1222
|
+
ref: { type: 'string', description: 'A1-style cell range the rule applies to.' },
|
|
1223
|
+
type: {
|
|
1224
|
+
type: 'string',
|
|
1225
|
+
description: 'Validation type. Server-side enum: whole, decimal, list, date, time, textLength, custom.',
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
required: ['sheet', 'ref', 'type'],
|
|
1229
|
+
},
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
required: ['session_id', 'validations'],
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1171
1235
|
];
|
|
1172
1236
|
|
|
1173
1237
|
// ---------------------------------------------------------------------------
|
|
@@ -1738,6 +1802,27 @@ async function dispatchTool(name, args) {
|
|
|
1738
1802
|
return applyFileB64(result, args.out_path);
|
|
1739
1803
|
}
|
|
1740
1804
|
|
|
1805
|
+
// Handle-based read (no file_b64; the bytes are already in the server
|
|
1806
|
+
// cache from a prior chunked-upload finalize). Body mirrors the server
|
|
1807
|
+
// schema in routes/xlsx-read-handle.ts.
|
|
1808
|
+
if (name === 'xlsx_read_handle') {
|
|
1809
|
+
const options = {};
|
|
1810
|
+
if (args.sheet !== undefined) options.sheet = args.sheet;
|
|
1811
|
+
if (args.format !== undefined) options.format = args.format;
|
|
1812
|
+
const body = { workbook_handle: args.workbook_handle };
|
|
1813
|
+
if (Object.keys(options).length > 0) body.options = options;
|
|
1814
|
+
return callTool('xlsx_read_handle', body);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Session-state write — no file bytes, just session_id + validation rules.
|
|
1818
|
+
// Body mirrors the server schema in routes/xlsx-session-set-validations.ts.
|
|
1819
|
+
if (name === 'xlsx_session_set_validations') {
|
|
1820
|
+
return callTool('xlsx_session_set_validations', {
|
|
1821
|
+
session_id: args.session_id,
|
|
1822
|
+
validations: args.validations,
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1741
1826
|
// All other tools (list_sheets, schema, hyperlinks, conditional_formats,
|
|
1742
1827
|
// styles, etc.) — single-file relay. Forward any common option keys the
|
|
1743
1828
|
// routes accept so we don't silently drop them. New keys added here as
|
|
@@ -1758,46 +1843,56 @@ async function dispatchTool(name, args) {
|
|
|
1758
1843
|
// ---------------------------------------------------------------------------
|
|
1759
1844
|
|
|
1760
1845
|
async function main() {
|
|
1761
|
-
|
|
1846
|
+
// Swallow EPIPE on the transport. When the client disconnects while a
|
|
1847
|
+
// background catalog upgrade is still in flight, sendToolListChanged
|
|
1848
|
+
// writes to a closed pipe and Node raises EPIPE asynchronously on the
|
|
1849
|
+
// Socket — our awaited try/catch around sendToolListChanged never sees
|
|
1850
|
+
// it. Without this guard, a client unplug after the upgrade settles
|
|
1851
|
+
// crashes the process with an unhandled Socket 'error' event.
|
|
1852
|
+
//
|
|
1853
|
+
// stdout is the MCP transport: EPIPE there means the client is gone,
|
|
1854
|
+
// exit cleanly. stderr is the log sink: an EPIPE on stderr (parent
|
|
1855
|
+
// closed its log pipe) is NOT a transport failure and must not take
|
|
1856
|
+
// the server down.
|
|
1857
|
+
process.stdout.on('error', (err) => {
|
|
1858
|
+
if (err && err.code === 'EPIPE') {
|
|
1859
|
+
process.exit(0);
|
|
1860
|
+
}
|
|
1861
|
+
// Anything else on the transport stream is a real failure (e.g.
|
|
1862
|
+
// ERR_STREAM_DESTROYED) — rethrow so it surfaces as uncaughtException
|
|
1863
|
+
// instead of being silently swallowed.
|
|
1864
|
+
throw err;
|
|
1865
|
+
});
|
|
1866
|
+
process.stderr.on('error', (err) => {
|
|
1867
|
+
// Silence EPIPE on stderr; rethrow anything else so we don't hide
|
|
1868
|
+
// genuine logging-layer bugs.
|
|
1869
|
+
if (!err || err.code !== 'EPIPE') throw err;
|
|
1870
|
+
});
|
|
1762
1871
|
|
|
1763
|
-
//
|
|
1764
|
-
//
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1872
|
+
// `initialize` MUST respond from local state — never block on the network.
|
|
1873
|
+
// Under Claude Desktop's bundled Node 24.x runtime, the registration POST
|
|
1874
|
+
// and the catalog GET can hang indefinitely (Happy-Eyeballs / IPv6 dial
|
|
1875
|
+
// edge cases inside Electron), and the client gives up at 60s. The whole
|
|
1876
|
+
// MCP attach dies before tools/list is even called.
|
|
1767
1877
|
//
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
),
|
|
1783
|
-
]);
|
|
1784
|
-
} catch (_) {
|
|
1785
|
-
catalog = { tools: TOOLS, source: 'static-fallback' };
|
|
1786
|
-
}
|
|
1787
|
-
// Surface catalog source so operators can tell server vs cache vs static
|
|
1788
|
-
// when an MCP session looks "off" (e.g., a tool missing because the remote
|
|
1789
|
-
// /api/v1/tools/list 404'd and we silently fell back to the stale baked-in
|
|
1790
|
-
// set). Stderr only — stdout is the MCP transport.
|
|
1791
|
-
process.stderr.write(`xlsx-for-ai-mcp: tool catalog source=${catalog.source} count=${Array.isArray(catalog.tools) ? catalog.tools.length : 0}\n`);
|
|
1792
|
-
// Overlay MCP annotations (title / readOnlyHint / destructiveHint) so
|
|
1793
|
-
// they flow through to clients regardless of catalog source. The remote
|
|
1794
|
-
// /api/v1/tools/list returns minimal entries today; this is what
|
|
1795
|
-
// restores the annotations the wire format would otherwise drop.
|
|
1796
|
-
const liveTools = applyAnnotations(Array.isArray(catalog.tools) ? catalog.tools : []);
|
|
1878
|
+
// Shape: connect transport FIRST with the bundled TOOLS as the floor.
|
|
1879
|
+
// Then background-upgrade registration + catalog with bounded timeouts,
|
|
1880
|
+
// and fire notifications/tools/list_changed once the live catalog lands.
|
|
1881
|
+
// The bundled set already covers every tool the user reaches in normal
|
|
1882
|
+
// flows; the upgrade is additive.
|
|
1883
|
+
// sanitizeForMcp guarantees every tool the server emits has a valid
|
|
1884
|
+
// inputSchema + description — without it Claude Desktop silently drops
|
|
1885
|
+
// tools that lack inputSchema, which is the exact symptom in SPM P0
|
|
1886
|
+
// 2026-06-05 (mcp-toolslist-missing-inputschema). For the bundled
|
|
1887
|
+
// catalog this is a no-op (every TOOLS entry already has full fields);
|
|
1888
|
+
// for the upgraded catalog it's the floor that keeps stub server
|
|
1889
|
+
// entries registerable.
|
|
1890
|
+
let liveTools = sanitizeForMcp(applyAnnotations(TOOLS));
|
|
1891
|
+
process.stderr.write(`xlsx-for-ai-mcp: tool catalog source=bundled count=${liveTools.length}\n`);
|
|
1797
1892
|
|
|
1798
1893
|
const server = new Server(
|
|
1799
1894
|
{ name: 'xlsx-for-ai', version: require('./package.json').version },
|
|
1800
|
-
{ capabilities: { tools: {} } }
|
|
1895
|
+
{ capabilities: { tools: { listChanged: true } } }
|
|
1801
1896
|
);
|
|
1802
1897
|
|
|
1803
1898
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: liveTools }));
|
|
@@ -1841,6 +1936,88 @@ async function main() {
|
|
|
1841
1936
|
|
|
1842
1937
|
const transport = new StdioServerTransport();
|
|
1843
1938
|
await server.connect(transport);
|
|
1939
|
+
|
|
1940
|
+
// Background-upgrade: registration + dynamic catalog. Bounded so a
|
|
1941
|
+
// hung network never wastes resources; failure is non-fatal because
|
|
1942
|
+
// the bundled catalog already serves tools/list. Detached on purpose
|
|
1943
|
+
// — we do not await this; main() returns and the upgrade lands when
|
|
1944
|
+
// it lands.
|
|
1945
|
+
upgradeCatalogInBackground(server, (next) => {
|
|
1946
|
+
liveTools = next;
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
async function withTimeout(promise, ms, label) {
|
|
1951
|
+
// Promise.race with a setTimeout-rejecting promise leaks unhandled
|
|
1952
|
+
// rejections in two directions:
|
|
1953
|
+
// (a) Main wins — the timer still fires later and its branch
|
|
1954
|
+
// rejects with nobody awaiting it. clearTimeout in finally
|
|
1955
|
+
// eliminates this.
|
|
1956
|
+
// (b) Timer wins — the original promise can still reject later
|
|
1957
|
+
// (the underlying fetch eventually errors out long after we
|
|
1958
|
+
// gave up). Attaching a no-op catch ensures that late
|
|
1959
|
+
// rejection is consumed instead of crashing the MCP server
|
|
1960
|
+
// minutes after startup.
|
|
1961
|
+
// The (b) case is the SPM P0 surface: the bundled-Node-24 dial
|
|
1962
|
+
// can stall, time out, and then much later reject with EAI_AGAIN
|
|
1963
|
+
// or a TLS error — by then nobody is listening.
|
|
1964
|
+
promise.catch(() => {});
|
|
1965
|
+
let timer;
|
|
1966
|
+
try {
|
|
1967
|
+
return await Promise.race([
|
|
1968
|
+
promise,
|
|
1969
|
+
new Promise((_, reject) => {
|
|
1970
|
+
timer = setTimeout(
|
|
1971
|
+
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
|
1972
|
+
ms
|
|
1973
|
+
);
|
|
1974
|
+
}),
|
|
1975
|
+
]);
|
|
1976
|
+
} finally {
|
|
1977
|
+
if (timer) clearTimeout(timer);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
async function upgradeCatalogInBackground(server, swap) {
|
|
1982
|
+
const REGISTRATION_TIMEOUT_MS = 10_000;
|
|
1983
|
+
const CATALOG_TIMEOUT_MS = 8_000;
|
|
1984
|
+
|
|
1985
|
+
try {
|
|
1986
|
+
await withTimeout(ensureRegistered(), REGISTRATION_TIMEOUT_MS, 'registration');
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
process.stderr.write(`xlsx-for-ai-mcp: registration deferred (${err.message})\n`);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
let catalog;
|
|
1992
|
+
try {
|
|
1993
|
+
catalog = await withTimeout(resolveCatalog(TOOLS), CATALOG_TIMEOUT_MS, 'catalog fetch');
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
process.stderr.write(`xlsx-for-ai-mcp: catalog upgrade skipped (${err.message})\n`);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (!catalog || !Array.isArray(catalog.tools)) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
// No upgrade to apply when discover.js fell back to the baked-in set
|
|
2003
|
+
// (source=static): the list is identical to what initialize already
|
|
2004
|
+
// returned, so a list_changed notification would be wire noise.
|
|
2005
|
+
if (catalog.source === 'static') {
|
|
2006
|
+
process.stderr.write(`xlsx-for-ai-mcp: catalog upgrade unavailable (source=static) — staying on bundled\n`);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const upgraded = sanitizeForMcp(applyAnnotations(catalog.tools));
|
|
2011
|
+
swap(upgraded);
|
|
2012
|
+
process.stderr.write(`xlsx-for-ai-mcp: tool catalog source=${catalog.source} count=${upgraded.length}\n`);
|
|
2013
|
+
|
|
2014
|
+
try {
|
|
2015
|
+
await server.sendToolListChanged();
|
|
2016
|
+
} catch (_) {
|
|
2017
|
+
// Transport may already be torn down (client disconnected before the
|
|
2018
|
+
// upgrade landed). Non-fatal — next attach starts with the bundled
|
|
2019
|
+
// catalog and retries the upgrade.
|
|
2020
|
+
}
|
|
1844
2021
|
}
|
|
1845
2022
|
|
|
1846
2023
|
// Guard: don't auto-start when required by tests
|
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.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": {
|