xlsx-for-ai 3.0.0 → 3.0.4

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 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 48 production-grade tools for reading, writing, diffing, redacting, healing, and cryptographically attesting `.xlsx` files — engine complexity runs server-side, engine IP stays private.
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-2.0.0.mcpb](https://github.com/senoff/xlsx-for-ai/releases/latest/download/xlsx-for-ai-2.0.0.mcpb)** *(latest release)*
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?" — 48 `xlsx_*` tools should appear, including `xlsx_doctor` (one-call health report — try it first on any unknown workbook).
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 6 tools.
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 the six xlsx tools are listed.
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
- 48 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).
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 36 read-only tools. Non-commercial use. |
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. |
@@ -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; baked-in tools fill gaps.
110
- * Order: every remote tool first (preserving server order), then any baked-in
111
- * tool whose name isn't in the remote set. This way the most up-to-date
112
- * description always wins, but we never lose a tool the client knows how to
113
- * dispatch even if the server temporarily forgets it.
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
- out.push(t);
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');
@@ -30,8 +30,7 @@ const TOOLS = [
30
30
  {
31
31
  name: 'xlsx_read',
32
32
  description:
33
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
34
- 'This tool: read an .xlsx file from the LOCAL filesystem and return a rendered markdown/JSON/SQL representation.\n' +
33
+ 'read an .xlsx file from the LOCAL filesystem and return a rendered markdown/JSON/SQL representation.\n' +
35
34
  'DEFAULT returns ALL sheets in one response — do not re-call per-sheet. Pass sheet="<name>" only when you specifically need to filter.\n' +
36
35
  'Pass format="md" (default — markdown table), "json" (structured rows), or "sql" (CREATE TABLE + INSERTs).\n' +
37
36
  'Synonyms accepted: "markdown" maps to "md", "text" maps to "md". Use the short forms to avoid guessing.\n\n' +
@@ -58,8 +57,7 @@ const TOOLS = [
58
57
  {
59
58
  name: 'xlsx_list_sheets',
60
59
  description:
61
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
62
- 'This tool: list sheet names, dimensions, and visibility for a LOCAL .xlsx file.\n' +
60
+ 'list sheet names, dimensions, and visibility for a LOCAL .xlsx file.\n' +
63
61
  'Use this when you only need names + dims, not cell content. If you\'ll read content anyway, skip this and call xlsx_read directly.\n\n' +
64
62
  'USE WHEN: the user references a LOCAL file path and you need to discover sheet names before reading. ' +
65
63
  'Fast orientation call — use before xlsx_read when you need metadata only.\n\n' +
@@ -76,8 +74,7 @@ const TOOLS = [
76
74
  {
77
75
  name: 'xlsx_schema',
78
76
  description:
79
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
80
- 'This tool: infer column schema of a LOCAL .xlsx file — types, nullable flags, header row, sample values.\n' +
77
+ 'infer column schema of a LOCAL .xlsx file types, nullable flags, header row, sample values.\n' +
81
78
  'Use when the agent needs to reason about column types BEFORE deciding how to handle data. Includes confidence (high/medium/low) per column.\n\n' +
82
79
  'USE WHEN: the user references a LOCAL file path and you need to understand column types before processing or writing code against the data. ' +
83
80
  'Useful before xlsx_read when downstream handling depends on types.\n\n' +
@@ -94,8 +91,7 @@ const TOOLS = [
94
91
  {
95
92
  name: 'xlsx_diff',
96
93
  description:
97
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
98
- 'This tool: compute a semantic diff between two LOCAL .xlsx files — cell-level deltas, formula changes, added/removed rows.\n' +
94
+ 'compute a semantic diff between two LOCAL .xlsx files — cell-level deltas, formula changes, added/removed rows.\n' +
99
95
  'Output is byte-deterministic — calling twice with the same inputs returns identical text + diff_hash in _meta. Use that hash for caching/idempotence.\n\n' +
100
96
  'USE WHEN: the user provides two LOCAL .xlsx file paths to compare. ' +
101
97
  'Suitable for version control, audit trails, and change review. Built-in skills cannot produce deterministic, structured diffs.\n\n' +
@@ -113,8 +109,7 @@ const TOOLS = [
113
109
  {
114
110
  name: 'xlsx_write',
115
111
  description:
116
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
117
- 'This tool: create or update a LOCAL .xlsx file from a structured spec.\n' +
112
+ 'create or update a LOCAL .xlsx file from a structured spec.\n' +
118
113
  'DEFAULT creates a new workbook from spec. Pass base_file_b64 to edit-in-place instead. Workbook bytes return in _meta.file_b64 (base64) — NOT in content[0].text.\n\n' +
119
114
  'ALWAYS pass out_path when the user wants the written file saved to disk.\n' +
120
115
  'WITHOUT out_path: workbook bytes return in _meta.file_b64 (base64) — caller must save them.\n' +
@@ -137,8 +132,7 @@ const TOOLS = [
137
132
  {
138
133
  name: 'xlsx_redact',
139
134
  description:
140
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
141
- 'This tool: redact PII and sensitive values from a LOCAL .xlsx file before sharing or archiving.\n' +
135
+ 'redact PII and sensitive values from a LOCAL .xlsx file before sharing or archiving.\n' +
142
136
  'DEFAULT preserves formulas + comments + named ranges + styles, strips only cell values. Pass strip_formulas=true / strip_comments=true to remove those too.\n\n' +
143
137
  'ALWAYS pass out_path when the user wants the redacted file saved to disk.\n' +
144
138
  'WITHOUT out_path: redacted bytes return in _meta.file_b64 (base64) — caller must save them.\n' +
@@ -167,8 +161,7 @@ const TOOLS = [
167
161
  {
168
162
  name: 'xlsx_describe',
169
163
  description:
170
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
171
- 'This tool: pandas-style df.describe() per column — count, nulls, unique, min/max/mean/std for numerics, dtype with purity score.\n' +
164
+ 'pandas-style df.describe() per column count, nulls, unique, min/max/mean/std for numerics, dtype with purity score.\n' +
172
165
  'Unlike pandas.read_excel followed by df.describe(), this does not silently flatten merged cells or drop named ranges.\n\n' +
173
166
  'USE WHEN: the user wants a quick summary of a LOCAL .xlsx file — "what\'s in this data?". ' +
174
167
  'Returns a markdown table with one row per column. Faster + more structured than dumping full contents through xlsx_read.\n\n' +
@@ -188,8 +181,7 @@ const TOOLS = [
188
181
  {
189
182
  name: 'xlsx_filter',
190
183
  description:
191
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
192
- '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' +
184
+ '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' +
193
185
  'Operates on real cell values — formulas evaluated server-side, not the cached results that pandas trusts blindly.\n\n' +
194
186
  'USE WHEN: the user asks for "rows where X" / "show me only Y" against a LOCAL .xlsx file. ' +
195
187
  'Returns matching rows as a markdown table, capped at 1000 rows by default with the actual match count.\n\n' +
@@ -223,8 +215,7 @@ const TOOLS = [
223
215
  {
224
216
  name: 'xlsx_aggregate',
225
217
  description:
226
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
227
- 'This tool: pandas-style df.groupby([cols]).agg({col: func}) on a LOCAL .xlsx file. funcs: sum / mean / min / max / count / count_distinct.\n' +
218
+ 'pandas-style df.groupby([cols]).agg({col: func}) on a LOCAL .xlsx file. funcs: sum / mean / min / max / count / count_distinct.\n' +
228
219
  'Type-aware: numeric aggregations skip non-numeric values cleanly instead of pandas\' silent NaN promotion.\n\n' +
229
220
  'USE WHEN: the user asks "what\'s the total / average / count of X by Y?" on a LOCAL .xlsx file. ' +
230
221
  'Returns one row per group with the requested aggregations as a markdown table.\n\n' +
@@ -260,8 +251,7 @@ const TOOLS = [
260
251
  {
261
252
  name: 'xlsx_named_ranges',
262
253
  description:
263
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
264
- 'This tool: list all defined names (named ranges) in a LOCAL .xlsx workbook — name, scope (workbook or sheet), kind (cell / range / formula), reference.\n' +
254
+ 'list all defined names (named ranges) in a LOCAL .xlsx workbook name, scope (workbook or sheet), kind (cell / range / formula), reference.\n' +
265
255
  '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' +
266
256
  'USE WHEN: the agent is reasoning about a financial / engineering model and needs to know what cells named-range references resolve to. ' +
267
257
  'Call before xlsx_read to orient.\n\n' +
@@ -278,8 +268,7 @@ const TOOLS = [
278
268
  {
279
269
  name: 'xlsx_sort',
280
270
  description:
281
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
282
- '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' +
271
+ 'pandas-style df.sort_values() on a LOCAL .xlsx file with multi-column sort and per-column direction (asc/desc, default asc).\n' +
283
272
  'Stable across all sort keys; type-aware comparison; nulls always sort last.\n\n' +
284
273
  'USE WHEN: the user wants rows ordered by one or more columns. Returns the sorted rows as a markdown table.\n\n' +
285
274
  'DO NOT USE WHEN: the data is already sorted as desired (use xlsx_read). Or for upload/attached files.',
@@ -311,8 +300,7 @@ const TOOLS = [
311
300
  {
312
301
  name: 'xlsx_value_counts',
313
302
  description:
314
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
315
- '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' +
303
+ 'pandas-style Series.value_counts() on one column of a LOCAL .xlsx file — count each unique value, sorted by frequency desc, with percentage.\n' +
316
304
  'Excludes nulls by default; pass include_nulls=true to count them.\n\n' +
317
305
  'USE WHEN: the user asks "what\'s the distribution of X?" / "how often does each value appear?". Returns a markdown table.\n\n' +
318
306
  'DO NOT USE WHEN: the user wants groupby + multi-column aggregations (use xlsx_aggregate). Or for upload/attached files.',
@@ -333,8 +321,7 @@ const TOOLS = [
333
321
  {
334
322
  name: 'xlsx_formulas',
335
323
  description:
336
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
337
- 'This tool: extract every formula in a LOCAL .xlsx workbook — cell coord (A1), formula text, cached result. openpyxl-style read-only metadata.\n' +
324
+ 'extract every formula in a LOCAL .xlsx workbook cell coord (A1), formula text, cached result. openpyxl-style read-only metadata.\n' +
338
325
  'Distinct from xlsx_read which returns evaluated values; this returns the formulas themselves so an agent can audit, transform, or rewrite them.\n\n' +
339
326
  'USE WHEN: the user wants to see what formulas a workbook uses — spot-checking a model, auditing references, debugging unexpected results. ' +
340
327
  'pandas cannot extract formulas; this is the only way for an agent to see them.\n\n' +
@@ -354,8 +341,7 @@ const TOOLS = [
354
341
  {
355
342
  name: 'xlsx_tables',
356
343
  description:
357
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
358
- 'This tool: list every Excel ListObject ("Format as Table" structures) in a LOCAL .xlsx workbook — name, sheet, range, header/totals flags, columns.\n' +
344
+ 'list every Excel ListObject ("Format as Table" structures) in a LOCAL .xlsx workbook name, sheet, range, header/totals flags, columns.\n' +
359
345
  'pandas cannot see ListObjects; if a workbook uses Excel Tables, this is the only way to enumerate them.\n\n' +
360
346
  'USE WHEN: the user references a "table" in a workbook by name, or you need to know what structured tables exist before reading. ' +
361
347
  'Useful for workbooks with multiple tables on one sheet.\n\n' +
@@ -374,8 +360,7 @@ const TOOLS = [
374
360
  {
375
361
  name: 'xlsx_pivot',
376
362
  description:
377
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
378
- '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' +
363
+ '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' +
379
364
  'agg modes: sum / mean / min / max / count / count_distinct. Optional fill_value for missing index×column combinations.\n\n' +
380
365
  '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' +
381
366
  'DO NOT USE WHEN: there\'s only one grouping dimension (use xlsx_aggregate). Or for upload/attached files.',
@@ -398,8 +383,7 @@ const TOOLS = [
398
383
  {
399
384
  name: 'xlsx_eval',
400
385
  description:
401
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
402
- 'This tool: evaluate Excel formulas against a LOCAL .xlsx file via HyperFormula. xlwings-style.\n' +
386
+ 'evaluate Excel formulas against a LOCAL .xlsx file via HyperFormula. xlwings-style.\n' +
403
387
  'Two modes: pass `formulas` (array of "=SUM(A1:A10)" expressions to compute against the workbook) or `cells` (array of "Sheet1!A1" cell refs to fresh-evaluate). Replaces pandas\' "trust the cached value" behavior with a real eval — if the cache is stale or missing, this still produces the right answer.\n\n' +
404
388
  'USE WHEN: the user wants the live computed value of a formula, not the cached one. Or when a workbook has formulas that depend on external data the cache might be stale on. ' +
405
389
  'Engine omits INDIRECT/HYPERLINK/WEBSERVICE/RTD/DDE by design — no I/O risk.\n\n' +
@@ -427,8 +411,7 @@ const TOOLS = [
427
411
  {
428
412
  name: 'xlsx_convert',
429
413
  description:
430
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
431
- 'This tool: universal spreadsheet format converter. Reads ANY of 25+ input formats (xlsx, xlsb, xlsm, xls, ods, fods, numbers, csv, tsv, dbf, lotus 1-2-3, quattro pro, sylk, dif, html, rtf, etc.) and emits ANY supported output format (xlsx, csv, json, md, html, etc.).\n' +
414
+ 'universal spreadsheet format converter. Reads ANY of 25+ input formats (xlsx, xlsb, xlsm, xls, ods, fods, numbers, csv, tsv, dbf, lotus 1-2-3, quattro pro, sylk, dif, html, rtf, etc.) and emits ANY supported output format (xlsx, csv, json, md, html, etc.).\n' +
432
415
  'No other tool in the MCP space ingests legacy formats — pandas.read_excel only reads xlsx/xls; openpyxl is xlsx-only. xlsx_convert is the only "any-spreadsheet → LLM-readable" hosted endpoint.\n\n' +
433
416
  'USE WHEN: the user has a .xls / .xlsb / .ods / Numbers / .csv / Lotus / Quattro / dBASE file they want to read or convert. ' +
434
417
  'Output to text formats (csv/json/md/html) renders into the response body for the agent to read directly. Output to binary formats (xlsx/xlsb/etc.) returns bytes in `_meta.file_b64` for the npm client to save.\n\n' +
@@ -457,12 +440,10 @@ const TOOLS = [
457
440
  {
458
441
  name: 'xlsx_data_clean',
459
442
  description:
460
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
461
- 'This tool: AI-native data cleaning. Scans a workbook for the seven most common data-grime issues — NA variants (N/A, NA, null, -), merged-cell residue, type-coercion mistakes (numeric-as-text / date-as-serial / leading-zero stripped), trailing-row noise (footers / totals), header-row-not-first (preamble before headers), encoding glitches (UTF-8-as-CP1252 mojibake like Café), and duplicate column headers — and either flags them (diagnose mode) or applies deterministic fixes (execute mode).\n' +
462
- 'No other tool gives this in a single call: pandas does ad-hoc fixes inline; openpyxl is structure-only; pre-existing Python "clean" libraries are domain-specific. xlsx_data_clean is the only single-call clean pipeline with an explicit informer-not-enforcer contract: every fix surfaces as a Finding the caller can accept / reject / scope-override before the file is mutated.\n\n' +
463
- 'USE WHEN: an upstream pipeline produced an xlsx that\'s about to feed an LLM or downstream analysis and you want a one-pass scrub. Or you just got a "messy" export (financial reports with merged title banners, CRM exports with stripped zip codes, survey data with NA-variant noise) and need it normalized before reading. ' +
464
- 'Free tier — counts against the 10k/mo cap.\n\n' +
465
- 'DO NOT USE WHEN: domain-specific transforms are needed (use a dedicated pipeline; this tool is general-purpose). Or for structural integrity checks (use xlsx_doctor). Or for upload/attached files.',
443
+ 'AI-native data cleaning for a LOCAL .xlsx file. Scans for the seven most common data-grime issues NA variants (N/A, NA, null, -), merged-cell residue, type-coercion mistakes (numeric-as-text / date-as-serial / leading-zero stripped), trailing-row noise (footers / totals), header-row-not-first (preamble before headers), encoding glitches (UTF-8-as-CP1252 mojibake), and duplicate column headers — and either flags them (diagnose mode) or applies deterministic fixes (execute mode).\n\n' +
444
+ 'Informer-not-enforcer: every fix surfaces as a Finding the caller can accept / reject / scope-override before the file is mutated.\n\n' +
445
+ 'USE WHEN: an upstream pipeline produced a messy xlsx that\'s about to feed an LLM or downstream analysis and you want a one-pass scrub.\n\n' +
446
+ 'DO NOT USE WHEN: domain-specific transforms are needed (use a dedicated pipeline). Or for structural integrity checks (use xlsx_doctor). Or for upload/attached files.',
466
447
  inputSchema: {
467
448
  type: 'object',
468
449
  properties: {
@@ -519,8 +500,7 @@ const TOOLS = [
519
500
  {
520
501
  name: 'xlsx_validate',
521
502
  description:
522
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
523
- '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' +
503
+ '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' +
524
504
  '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' +
525
505
  '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. ' +
526
506
  'PAID — Bronze / Silver / Gold tier required.\n\n' +
@@ -537,8 +517,7 @@ const TOOLS = [
537
517
  {
538
518
  name: 'xlsx_data_validations',
539
519
  description:
540
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
541
- 'This tool: list every cell-level data validation rule (dropdowns, numeric/date bounds, text-length caps, custom formulas) defined in a workbook — the constraints that Excel enforces when a human types into the cell.\n' +
520
+ 'list every cell-level data validation rule (dropdowns, numeric/date bounds, text-length caps, custom formulas) defined in a workbook — the constraints that Excel enforces when a human types into the cell.\n' +
542
521
  'No other tool can do this: pandas drops validations entirely on read; openpyxl exposes them but only on a per-cell loop; this surfaces them in one shot with target cells, formulae, error messages, and prompt text.\n\n' +
543
522
  'USE WHEN: auditing a form / data-entry workbook to know what inputs are legal. Or extracting a dropdown list for use elsewhere. Or generating fixtures that match the validation contract. ' +
544
523
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -556,8 +535,7 @@ const TOOLS = [
556
535
  {
557
536
  name: 'xlsx_hyperlinks',
558
537
  description:
559
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
560
- 'This tool: list every hyperlink in a workbook with its anchor cell, target URL/anchor, display text, tooltip, and a kind classifier (external / internal / mailto / unknown).\n' +
538
+ 'list every hyperlink in a workbook with its anchor cell, target URL/anchor, display text, tooltip, and a kind classifier (external / internal / mailto / unknown).\n' +
561
539
  'No other tool can do this: pandas drops hyperlinks on read entirely; openpyxl gives raw access but does not classify or aggregate; this surfaces all links plus a per-kind tally for instant audit.\n\n' +
562
540
  'USE WHEN: security-auditing a workbook before opening it (what URLs does it point at?). Or extracting a reference list of URLs from a financial model / dashboard. Or finding mailto links for a contact-list workbook. ' +
563
541
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -575,8 +553,7 @@ const TOOLS = [
575
553
  {
576
554
  name: 'xlsx_topology',
577
555
  description:
578
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
579
- 'This tool: one-call workbook orientation. Returns sheets × dimensions × formulas × named ranges × tables × validations × hyperlinks × merges in one shot, plus feature flags (macros / external refs / pivots / LAMBDA / dynamic arrays).\n' +
556
+ 'one-call workbook orientation. Returns sheets × dimensions × formulas × named ranges × tables × validations × hyperlinks × merges in one shot, plus feature flags (macros / external refs / pivots / LAMBDA / dynamic arrays).\n' +
580
557
  'No other tool can do this: pandas gives you a frame per sheet but no structure; openpyxl makes you fan out across 6+ object trees to learn the same thing; this is the "what is in this workbook?" call you make first to decide which other tool to call next.\n\n' +
581
558
  'USE WHEN: an agent has just been handed a workbook and needs to orient before drilling in. Or surveying many workbooks for triage / index. Or auditing whether a workbook is "interesting" (formulas? macros? external refs?). ' +
582
559
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -593,8 +570,7 @@ const TOOLS = [
593
570
  {
594
571
  name: 'xlsx_conditional_formats',
595
572
  description:
596
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
597
- 'This tool: list every conditional formatting rule in a workbook — color scales, data bars, icon sets, formula-based highlights, top-N, duplicate / unique values, contains-text, time-period, above-average. Per rule: range, type, operator, formulae, priority, stopIfTrue.\n' +
573
+ 'list every conditional formatting rule in a workbook color scales, data bars, icon sets, formula-based highlights, top-N, duplicate / unique values, contains-text, time-period, above-average. Per rule: range, type, operator, formulae, priority, stopIfTrue.\n' +
598
574
  'No other tool can do this: pandas drops conditional formatting on read entirely; openpyxl exposes the raw CF objects but offers no rollup or classification. This surfaces every rule plus a per-type tally so an agent can answer "does this workbook use color scales?" without scanning every row.\n\n' +
599
575
  'USE WHEN: auditing a dashboard / financial model to know what visual cues a human would see. Or extracting business rules embedded as CF (e.g. "row turns red when col C > 1000" — the rule IS the spec). Or generating fixtures that match a workbook\'s CF semantics. ' +
600
576
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -612,8 +588,7 @@ const TOOLS = [
612
588
  {
613
589
  name: 'xlsx_comments',
614
590
  description:
615
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
616
- 'This tool: list every cell comment in a workbook — both legacy notes (yellow stickies, cell.note) AND modern threaded comments (multi-author conversations stored separately in the OOXML zip). Per entry: kind, sheet, cell, author, text, plus any reply thread.\n' +
591
+ 'list every cell comment in a workbook both legacy notes (yellow stickies, cell.note) AND modern threaded comments (multi-author conversations stored separately in the OOXML zip). Per entry: kind, sheet, cell, author, text, plus any reply thread.\n' +
617
592
  'No other tool can do this: pandas drops both comment systems on read entirely; openpyxl reads only legacy notes (not threaded comments). xlsx_comments reads both, maps personId → display name via xl/persons/person.xml, and folds reply chains into each root comment.\n\n' +
618
593
  'USE WHEN: extracting reviewer feedback / approval threads from a spreadsheet (this is where humans hide intent). Or auditing a workbook for hidden context the values themselves don\'t carry. Or building a "show me everywhere finance flagged something" report. ' +
619
594
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -631,12 +606,10 @@ const TOOLS = [
631
606
  {
632
607
  name: 'xlsx_doctor',
633
608
  description:
634
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
635
- 'This tool: ONE-CALL workbook health report. Scans for macros, external workbook references, hidden / veryHidden sheets, missing creator metadata, large embedded images, and surfaces interesting feature flags (LAMBDA, dynamic arrays, pivot cache, slicers, threaded comments). Findings ranked HIGH / MEDIUM / LOW. Plus quick_facts: sheet count, formulas, named ranges, merges, hyperlinks, validations, images, file size.\n' +
636
- 'No other tool does this aggregate triage in a single call. The "check this workbook" call agents should make BEFORE any other tool — single round trip, exhaustive, ranked output an LLM can read at a glance and decide what to do next (e.g. "macros detected, run xlsx_macros for details, then warn the user").\n\n' +
637
- 'USE WHEN: an agent has been handed an unknown workbook and needs to triage it before drilling in. Or pre-flighting a file that\'s about to be shared (catches "I\'m about to email this and didn\'t notice the macros" mistakes). Or building a file-quality dashboard across many workbooks. ' +
638
- 'Free tier — counts against the 10k/mo cap.\n\n' +
639
- 'DO NOT USE WHEN: you already know what you\'re looking for (use the focused tool instead — xlsx_macros, xlsx_external_links, etc.). Or you only need data values (use xlsx_read).',
609
+ 'ONE-CALL workbook health report for a LOCAL .xlsx file. Scans for macros, external workbook references, hidden / veryHidden sheets, missing creator metadata, large embedded images, and surfaces interesting feature flags (LAMBDA, dynamic arrays, pivot cache, slicers, threaded comments). Findings ranked HIGH / MEDIUM / LOW. Plus quick_facts: sheet count, formulas, named ranges, merges, hyperlinks, validations, images, file size.\n\n' +
610
+ 'The "check this workbook" call agents should make BEFORE any other tool single round trip, ranked output an LLM can read at a glance.\n\n' +
611
+ 'USE WHEN: an agent has been handed an unknown workbook and needs to triage it before drilling in. Or pre-flighting a file before sharing.\n\n' +
612
+ 'DO NOT USE WHEN: you already know what you\'re looking for (use the focused tool xlsx_macros, xlsx_external_links, etc.). Or you only need data values (use xlsx_read).',
640
613
  inputSchema: {
641
614
  type: 'object',
642
615
  properties: {
@@ -649,8 +622,7 @@ const TOOLS = [
649
622
  {
650
623
  name: 'xlsx_form_controls',
651
624
  description:
652
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
653
- 'This tool: list every form control (Check Box, Button, Drop-down, List Box, Option Button, Scroll Bar, Spinner, Label, Group Box) in a workbook with the linked cell, current value, dropdown source range, and min/max/step bounds where applicable.\n' +
625
+ 'list every form control (Check Box, Button, Drop-down, List Box, Option Button, Scroll Bar, Spinner, Label, Group Box) in a workbook with the linked cell, current value, dropdown source range, and min/max/step bounds where applicable.\n' +
654
626
  'No other tool gives this in a single call: ExcelJS doesn\'t expose form controls; pandas drops them entirely; openpyxl support is partial. xlsx_form_controls reads xl/ctrlProps/ctrlProp*.xml directly + maps to sheets via the rel chain.\n\n' +
655
627
  'USE WHEN: documenting a survey workbook, scoring rubric, dashboard, or forms-as-spreadsheets template where the interactive UI carries semantic meaning. Or auditing a workbook to find which cells human users can change via a control vs. by direct typing. ' +
656
628
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -668,12 +640,9 @@ const TOOLS = [
668
640
  {
669
641
  name: 'xlsx_macros',
670
642
  description:
671
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
672
- 'This tool: inspect xlsm / xlsb workbooks for VBA macro presence, vbaProject.bin size, and likely module names (ThisWorkbook / Sheet<N> / Module<N> / Class<N> / UserForm<N> via heuristic UTF-16LE scan). Returns short safety advice for the LLM to relay to the user.\n' +
673
- 'No other tool gives this in a single call: pandas drops macros entirely; openpyxl exposes the raw vbaProject.bin bytes but no usable inspection. xlsx_macros gives the security-audit metadata an agent (or human) needs to decide "should I trust this file?" before opening.\n\n' +
674
- 'By DELIBERATE POLICY this tool does NOT extract or execute macro source code. Surfaces presence + module name candidates only.\n\n' +
675
- 'USE WHEN: receiving a macro-enabled workbook from an unknown sender and you want to know what to expect before opening. Or auditing a workbook population for "do any of these contain macros?" without sampling each. ' +
676
- 'Free tier — counts against the 10k/mo cap.\n\n' +
643
+ 'Inspect xlsm / xlsb workbooks for VBA macro presence, vbaProject.bin size, and likely module names (ThisWorkbook / Sheet<N> / Module<N> / Class<N> / UserForm<N> via heuristic UTF-16LE scan). Returns short safety advice the LLM should relay to the user.\n\n' +
644
+ 'By DELIBERATE POLICY this tool does NOT extract or execute macro source code. Surfaces presence + module-name candidates only security-audit metadata for "should I trust this file?" decisions.\n\n' +
645
+ 'USE WHEN: receiving a macro-enabled workbook from an unknown sender and you want to know what to expect before opening. Or auditing many workbooks for "do any of these contain macros?" without sampling each.\n\n' +
677
646
  'DO NOT USE WHEN: you need to actually inspect / debug VBA source — open the file in Excel (Alt+F11) on a trusted machine.',
678
647
  inputSchema: {
679
648
  type: 'object',
@@ -687,8 +656,7 @@ const TOOLS = [
687
656
  {
688
657
  name: 'xlsx_merged_cells',
689
658
  description:
690
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
691
- 'This tool: list every merged-cell region with master-cell value, range, span dimensions, and kind heuristic ("header" / "horizontal" / "vertical" / "block"). Pandas reads merged cells by dropping the relationship — it sees one value in the master cell and three blanks alongside. xlsx_merged_cells is the layout-aware view: "A1:D1 is ONE cell that says Q4 2024" rather than four cells where three are mysteriously empty.\n' +
659
+ 'list every merged-cell region with master-cell value, range, span dimensions, and kind heuristic ("header" / "horizontal" / "vertical" / "block"). Pandas reads merged cells by dropping the relationship — it sees one value in the master cell and three blanks alongside. xlsx_merged_cells is the layout-aware view: "A1:D1 is ONE cell that says Q4 2024" rather than four cells where three are mysteriously empty.\n' +
692
660
  'No other tool surfaces merges with master values rolled in: pandas drops merge metadata; openpyxl exposes ranges but not the master value alongside.\n\n' +
693
661
  'USE WHEN: parsing report templates, dashboards, or form workbooks where merges encode visual hierarchy (section titles, sub-headers, banner rows). Or auditing a workbook for accidental merges that distort downstream pandas reads. ' +
694
662
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -706,11 +674,9 @@ const TOOLS = [
706
674
  {
707
675
  name: 'xlsx_workbook_views',
708
676
  description:
709
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
710
- 'This tool: surface the UI state of a workbook — what a human sees when they open the file in Excel. Per sheet: visibility (visible / hidden / veryHidden), view state (normal / frozen / split / pageBreakPreview / pageLayout), zoom level, active cell + selection range, frozen pane breakdown (rows/cols frozen + top-left of scroll area), gridlines / row-col headers / ruler / RTL flags, tab color. Workbook level: which sheet is the active tab when Excel opens.\n' +
711
- 'No other tool surfaces this: pandas drops every bit of UI state; openpyxl exposes view objects but in deeply nested form. xlsx_workbook_views is the "when the user opens this file, what do they see?" rollup an LLM needs to reason about continuity (resume editing where they left off, notice a hidden sheet exists, etc.).\n\n' +
712
- 'USE WHEN: an agent has been handed a workbook mid-workflow and needs to know "where was the user last working?" (active cell, active tab, zoom). Or auditing for hidden / veryHidden sheets that often hide sensitive data. Or extracting frozen-pane configuration to recreate the same UX in a generated workbook. ' +
713
- 'Free tier — counts against the 10k/mo cap.\n\n' +
677
+ 'Surface the UI state of a LOCAL .xlsx file what a human sees when they open it in Excel. Per sheet: visibility (visible / hidden / veryHidden), view state, zoom, active cell + selection, frozen-pane breakdown, gridlines / row-col headers / ruler / RTL flags, tab color. Workbook level: which sheet is active when Excel opens.\n\n' +
678
+ 'The "when the user opens this file, what do they see?" rollup useful when an agent needs to reason about UI continuity (resume editing, notice a hidden sheet, replicate frozen panes in a generated workbook).\n\n' +
679
+ 'USE WHEN: handed a workbook mid-workflow and need "where was the user last working?" (active cell, tab, zoom). Or auditing for hidden / veryHidden sheets that often conceal sensitive data.\n\n' +
714
680
  'DO NOT USE WHEN: just reading values (use xlsx_read).',
715
681
  inputSchema: {
716
682
  type: 'object',
@@ -725,8 +691,7 @@ const TOOLS = [
725
691
  {
726
692
  name: 'xlsx_print_settings',
727
693
  description:
728
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
729
- 'This tool: surface "what would Excel print right now" per worksheet — print area, orientation, paper size (A4 / Letter / Legal / Tabloid / etc.), scale or fitToPage, margins, headers/footers split into Excel\'s L/C/R zones, print titles (rows / columns repeated on every page), manual page breaks, plus B&W / draft / centered flags.\n' +
694
+ 'surface "what would Excel print right now" per worksheet print area, orientation, paper size (A4 / Letter / Legal / Tabloid / etc.), scale or fitToPage, margins, headers/footers split into Excel\'s L/C/R zones, print titles (rows / columns repeated on every page), manual page breaks, plus B&W / draft / centered flags.\n' +
730
695
  'No other tool can do this rolled-up: pandas drops every bit of print configuration; openpyxl exposes it but in nested object form. xlsx_print_settings is the "if a human hits Cmd+P, what comes out?" answer.\n\n' +
731
696
  'USE WHEN: about to PDF / print a workbook and want to know what it\'ll look like before doing it. Or auditing a financial / regulatory report\'s print configuration (legal sometimes cares about page-1 headers). Or extracting the print-titles row a complex workbook uses for repeating headers. ' +
732
697
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -744,12 +709,10 @@ const TOOLS = [
744
709
  {
745
710
  name: 'xlsx_properties',
746
711
  description:
747
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
748
- 'This tool: surface the workbook\'s identity card. Core: creator, last_modified_by, created/modified/lastPrinted timestamps, title, subject, company, manager, keywords, category, description. Application: app name, app version, doc security label, hyperlink base. Custom: every user-defined Info > Properties entry (Department, ReviewedBy, ApprovalRequired, etc.) with type tag and value.\n' +
749
- 'No other tool gives you this rolled up: pandas drops document properties entirely; openpyxl exposes core props but in nested object form unsuitable for LLM consumption. Reads docProps/core.xml, docProps/app.xml, and docProps/custom.xml directly.\n\n' +
750
- 'USE WHEN: auditing a workbook for attribution ("who built this and when?"). Or stripping sensitive metadata before sharing externally (creator names, internal company names, manager email). Or extracting custom finance/legal flags ("ReviewedBy", "ApprovalRequired") that workflows pin to the file. ' +
751
- 'Free tier — counts against the 10k/mo cap.\n\n' +
752
- 'DO NOT USE WHEN: just reading values (use xlsx_read). Or trying to MODIFY metadata (use xlsx_redact for sensitive-field stripping; xlsx_write does not write doc props).',
712
+ 'Surface the workbook\'s identity card from a LOCAL .xlsx file. Core: creator, last_modified_by, created/modified/lastPrinted timestamps, title, subject, company, manager, keywords, category, description. Application: app name + version, doc security label, hyperlink base. Custom: every user-defined Info > Properties entry (Department, ReviewedBy, ApprovalRequired, etc.) with type tag and value.\n\n' +
713
+ 'Reads docProps/core.xml, docProps/app.xml, and docProps/custom.xml directly a surface pandas drops entirely.\n\n' +
714
+ 'USE WHEN: auditing a workbook for attribution ("who built this and when?"). Or stripping sensitive metadata before sharing externally. Or extracting custom finance/legal flags ("ReviewedBy", "ApprovalRequired") that workflows pin to the file.\n\n' +
715
+ 'DO NOT USE WHEN: just reading values (use xlsx_read). Or trying to MODIFY metadata (use xlsx_redact for sensitive-field stripping).',
753
716
  inputSchema: {
754
717
  type: 'object',
755
718
  properties: {
@@ -762,8 +725,7 @@ const TOOLS = [
762
725
  {
763
726
  name: 'xlsx_external_links',
764
727
  description:
765
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
766
- 'This tool: list every external workbook reference this file depends on — `=[Budget.xlsx]Sheet1!A1` style formulas. Per link: target path (decoded), classification (http / network share / absolute / relative), sheets pulled from the external workbook, count of cached cell values, and defined-name references.\n' +
728
+ 'list every external workbook reference this file depends on `=[Budget.xlsx]Sheet1!A1` style formulas. Per link: target path (decoded), classification (http / network share / absolute / relative), sheets pulled from the external workbook, count of cached cell values, and defined-name references.\n' +
767
729
  'No other tool can do this consistently: pandas, openpyxl, and ExcelJS all surface external links partially or inconsistently. xlsx_external_links reads xl/externalLinks/*.xml directly and warns when targets are absolute paths or network shares — those break the moment the workbook moves elsewhere.\n\n' +
768
730
  'USE WHEN: about to send a workbook somewhere and want to know if its formulas will break (broken external refs are a top-3 silent corruption mode in finance workflows). Or auditing for accidentally-leaked file paths to internal network shares. Or doing dependency analysis on a model. ' +
769
731
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -780,11 +742,9 @@ const TOOLS = [
780
742
  {
781
743
  name: 'xlsx_slicers_timelines',
782
744
  description:
783
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
784
- 'This tool: list every slicer (interactive filter button) and timeline (date-range filter visual) in a workbook with their captions, source bindings (table column or pivot table), and timeline granularity (years / quarters / months / days) plus the currently-selected date range.\n' +
785
- 'No other tool can do this: ExcelJS has NO API for slicers or timelines and silently drops both on every round-trip; pandas drops them entirely; openpyxl support is partial. xlsx_slicers_timelines reads the OOXML zip (xl/slicers/*, xl/slicerCaches/*, xl/timelines/*, xl/timelineCaches/*) directly.\n\n' +
786
- 'USE WHEN: documenting a dashboard so an LLM knows what filter UI a human sees. Or auditing whether a slicer\'s table-column binding still matches the underlying data after a refactor. Or extracting the date range a timeline currently filters on without screenshotting Excel. ' +
787
- 'Free tier — counts against the 10k/mo cap.\n\n' +
745
+ 'List every slicer (interactive filter button) and timeline (date-range filter visual) in a LOCAL .xlsx file with their captions, source bindings (table column or pivot table), and timeline granularity (years / quarters / months / days) plus the currently-selected date range.\n\n' +
746
+ 'Reads the OOXML zip (xl/slicers/*, xl/slicerCaches/*, xl/timelines/*, xl/timelineCaches/*) directly a surface ExcelJS silently drops on round-trip.\n\n' +
747
+ 'USE WHEN: documenting a dashboard so an LLM knows what filter UI a human sees. Or auditing whether a slicer\'s binding still matches the underlying data after a refactor.\n\n' +
788
748
  'DO NOT USE WHEN: just reading values (use xlsx_read). Or trying to APPLY a filter (use xlsx_filter — slicers/timelines are UI metadata, not data filters).',
789
749
  inputSchema: {
790
750
  type: 'object',
@@ -798,11 +758,9 @@ const TOOLS = [
798
758
  {
799
759
  name: 'xlsx_pivot_tables',
800
760
  description:
801
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
802
- 'This tool: list every PRE-EXISTING pivot table definition in a workbook (the ones an Excel user already built). Per pivot: sheet, name, location range, source range (or named-range / table reference), row / column / page fields, and data fields with their agg function (sum / count / average / max / min / product / stdDev / etc.).\n' +
803
- 'No other tool can do this: ExcelJS doesn\'t expose pivot tables; pandas drops them entirely; openpyxl reads them but in a deeply-nested object model unsuitable for LLM consumption. Distinct from `xlsx_pivot` which COMPUTES a fresh pivot from raw data this tool surfaces the existing pivot CONTRACT so an agent can answer "what does PivotTable3 on the Summary sheet actually compute?".\n\n' +
804
- 'USE WHEN: documenting a financial model that uses pivot tables. Or auditing whether a pivot still points at the right source range after a data-table refactor. Or answering "which sheet aggregates Sales by Region?" without re-deriving it. ' +
805
- 'Free tier — counts against the 10k/mo cap.\n\n' +
761
+ 'List every PRE-EXISTING pivot table definition in a LOCAL .xlsx file (the ones an Excel user already built). Per pivot: sheet, name, location range, source range (or named-range / table reference), row / column / page fields, and data fields with their agg function (sum / count / average / max / min / product / stdDev / etc.).\n\n' +
762
+ 'Distinct from `xlsx_pivot` which COMPUTES a fresh pivot from raw data this tool surfaces the existing pivot CONTRACT so an agent can answer "what does PivotTable3 on the Summary sheet actually compute?".\n\n' +
763
+ 'USE WHEN: documenting a financial model that uses pivot tables. Or auditing whether a pivot still points at the right source range after a data refactor. Or answering "which sheet aggregates Sales by Region?" without re-deriving it.\n\n' +
806
764
  'DO NOT USE WHEN: you want to COMPUTE a fresh pivot from raw data (use xlsx_pivot). Or you only need cell values (use xlsx_read).',
807
765
  inputSchema: {
808
766
  type: 'object',
@@ -817,12 +775,10 @@ const TOOLS = [
817
775
  {
818
776
  name: 'xlsx_images',
819
777
  description:
820
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
821
- 'This tool: list every embedded image in a workbook with format (png / jpg / gif / svg / bmp / tiff / emf / wmf), size in bytes, sheet attribution, and anchor cell range (the cells the image floats over). Reads xl/media/* + xl/drawings/* directly.\n' +
822
- 'No other tool can do this in one call: pandas drops images entirely; openpyxl reads images but doesn\'t roll them up by sheet/format/size or surface anchor cell refs in human-readable form. xlsx_images surfaces "Sheet1 has a 4 KB PNG anchored at B2:D6" — the exact thing an LLM needs to know whether the workbook ships with branding / charts-as-images / signatures.\n\n' +
823
- 'USE WHEN: cataloging the visual assets in a financial / operational workbook. Or auditing a workbook for embedded images that need to be replaced (logos changed, signatures rotated). Or fingerprinting a template by its image inventory. ' +
824
- 'Free tier — counts against the 10k/mo cap.\n\n' +
825
- 'DO NOT USE WHEN: you want the image PIXELS (this surfaces metadata, not bytes — fetching the bytes would inflate the response well beyond LLM context budgets). Or you only need cell values (use xlsx_read).',
778
+ 'List every embedded image in a LOCAL .xlsx file with format (png / jpg / gif / svg / bmp / tiff / emf / wmf), size in bytes, sheet attribution, and anchor cell range (the cells the image floats over). Reads xl/media/* + xl/drawings/* directly.\n\n' +
779
+ 'Surfaces "Sheet1 has a 4 KB PNG anchored at B2:D6" what an LLM needs to know whether the workbook ships with branding / charts-as-images / signatures.\n\n' +
780
+ 'USE WHEN: cataloging visual assets. Or auditing a workbook for embedded images that need to be replaced (logos, signatures). Or fingerprinting a template by its image inventory.\n\n' +
781
+ 'DO NOT USE WHEN: you want the image PIXELS (this surfaces metadata, not bytes). Or you only need cell values (use xlsx_read).',
826
782
  inputSchema: {
827
783
  type: 'object',
828
784
  properties: {
@@ -836,12 +792,10 @@ const TOOLS = [
836
792
  {
837
793
  name: 'xlsx_charts',
838
794
  description:
839
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
840
- 'This tool: list every chart in a workbook with type (bar / line / pie / scatter / area / doughnut / radar / stock / surface / bubble), title, axis titles, and per-series formula refs (the cell ranges the chart pulls from). Sheet attribution via the OOXML drawing rel chain.\n' +
841
- 'No other tool can do this: ExcelJS doesn\'t expose charts at all (read or write); pandas drops them entirely; openpyxl reads charts but in a deeply-nested object form unsuitable for LLM consumption. xlsx_charts gives you the chart contract "Sheet2 has a bar chart titled Q4 Revenue plotting Sheet1!B2:B10 against Sheet1!A2:A10" — without rendering anything.\n\n' +
842
- 'USE WHEN: documenting a financial model / dashboard for an LLM that needs to know "what does this workbook visualize, and from which cells?". Or auditing a workbook for chart-data drift after a refactor (chart still points at old range?). ' +
843
- 'Free tier — counts against the 10k/mo cap.\n\n' +
844
- 'DO NOT USE WHEN: you want to RENDER the chart as an image (this tool returns the chart spec, not pixels). Or you only need cell values (use xlsx_read).',
795
+ 'List every chart in a LOCAL .xlsx file with type (bar / line / pie / scatter / area / doughnut / radar / stock / surface / bubble), title, axis titles, and per-series formula refs (the cell ranges the chart pulls from). Sheet attribution via the OOXML drawing rel chain.\n\n' +
796
+ 'Gives you the chart contract "Sheet2 has a bar chart titled Q4 Revenue plotting Sheet1!B2:B10 against Sheet1!A2:A10" without rendering anything.\n\n' +
797
+ 'USE WHEN: documenting a financial model / dashboard so an LLM knows "what does this visualize, from which cells?". Or auditing for chart-data drift after a refactor.\n\n' +
798
+ 'DO NOT USE WHEN: you want to RENDER the chart as an image (this returns the spec, not pixels). Or you only need cell values (use xlsx_read).',
845
799
  inputSchema: {
846
800
  type: 'object',
847
801
  properties: {
@@ -855,12 +809,10 @@ const TOOLS = [
855
809
  {
856
810
  name: 'xlsx_protection',
857
811
  description:
858
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
859
- 'This tool: surface every protection setting in a workbook so an agent knows what it can and cannot edit. Workbook-level (lockStructure, lockWindows), per-sheet (protected? password? hidden state?), per-action allow/block list (formatCells, sort, insertRows, pivotTables, etc.), and per-cell unlocked / hidden samples — these are the cells a human would actually be allowed to type into when the sheet is otherwise read-only.\n' +
860
- 'No other tool can do this: pandas drops protection metadata entirely; openpyxl exposes the bool but no normalization. xlsx_protection reads sheetProtection action attrs directly from the OOXML zip (workaround for ExcelJS stripping them on round-trip).\n\n' +
861
- 'USE WHEN: an agent is about to suggest edits to a workbook and you want to fail fast on cells / sheets the user can\'t change anyway. Or auditing a "submitted form" workbook to see which inputs the form-author intended to be fillable. ' +
862
- 'Free tier — counts against the 10k/mo cap.\n\n' +
863
- 'DO NOT USE WHEN: just reading values (use xlsx_read). Or trying to BREAK protection (this tool surfaces what\'s locked; it does not unlock).',
812
+ 'Surface every protection setting in a LOCAL .xlsx file so an agent knows what it can and cannot edit. Workbook-level (lockStructure, lockWindows), per-sheet (protected? password? hidden state?), per-action allow/block list (formatCells, sort, insertRows, pivotTables, etc.), and per-cell unlocked / hidden samples — these are the cells a human would actually be allowed to type into when the sheet is otherwise read-only.\n\n' +
813
+ 'Reads sheetProtection action attrs directly from the OOXML zip (workaround for ExcelJS stripping them on round-trip).\n\n' +
814
+ 'USE WHEN: an agent is about to suggest edits and you want to fail fast on cells / sheets the user can\'t change anyway. Or auditing a "submitted form" workbook to see which inputs the author intended fillable.\n\n' +
815
+ 'DO NOT USE WHEN: just reading values (use xlsx_read). Or trying to BREAK protection (this surfaces what\'s locked; it does not unlock).',
864
816
  inputSchema: {
865
817
  type: 'object',
866
818
  properties: {
@@ -874,8 +826,7 @@ const TOOLS = [
874
826
  {
875
827
  name: 'xlsx_styles',
876
828
  description:
877
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
878
- 'This tool: surface cell formatting (number formats, fonts, fills, alignment) so an agent knows what a cell LOOKS like, not just its raw value. Default mode: per-sheet rollup of top-N number formats / fonts / fills with counts. Detailed mode (opt-in, capped at 1000 cells): per-cell breakdown for narrow queries.\n' +
829
+ 'surface cell formatting (number formats, fonts, fills, alignment) so an agent knows what a cell LOOKS like, not just its raw value. Default mode: per-sheet rollup of top-N number formats / fonts / fills with counts. Detailed mode (opt-in, capped at 1000 cells): per-cell breakdown for narrow queries.\n' +
879
830
  'No other tool can do this with this fidelity: pandas drops styles on read entirely. The single most valuable slice is number formats — pandas hands an LLM "45292" and the cell rendered as "2024-01-01" because format was "yyyy-mm-dd". xlsx_styles is what makes that recoverable.\n\n' +
880
831
  'USE WHEN: an LLM is about to interpret raw numbers (date serials, currency, percents, scientific notation) and you want the format hint that tells it what those numbers MEAN to a human. Or auditing a dashboard\'s typography. Or fingerprinting a template. ' +
881
832
  'Free tier — counts against the 10k/mo cap.\n\n' +
@@ -894,8 +845,7 @@ const TOOLS = [
894
845
  {
895
846
  name: 'xlsx_post_slack',
896
847
  description:
897
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
898
- 'This tool: upload a local .xlsx file to a Slack channel as a file attachment, with an optional accompanying message.\n' +
848
+ 'upload a local .xlsx file to a Slack channel as a file attachment, with an optional accompanying message.\n' +
899
849
  'Token intake: set SLACK_BOT_TOKEN in the environment (recommended — keeps the token out of conversation logs). ' +
900
850
  'Alternatively pass slack_token as a tool argument (legacy; token will appear in MCP conversation history).\n' +
901
851
  'Posts via Slack\'s 3-step external upload flow (files.getUploadURLExternal → upload → files.completeUploadExternal), which is the only sanctioned path as of 2024+.\n\n' +
@@ -918,14 +868,10 @@ const TOOLS = [
918
868
  {
919
869
  name: 'xlsx_post_teams',
920
870
  description:
921
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
922
- 'This tool: upload a local .xlsx file to a Microsoft Teams channel as a file attachment in a channel message, with an optional accompanying message.\n' +
923
- 'Token intake: set TEAMS_GRAPH_TOKEN in the environment (recommended keeps the token out of conversation logs). ' +
924
- 'Alternatively pass graph_token as a tool argument (legacy; token will appear in MCP conversation history).\n' +
925
- 'Uses Microsoft Graph\'s 4-step flow: locate the channel\'s filesFolder driveItem, create an upload session, upload the bytes, then post a chatMessage with the file as an inline attachment.\n\n' +
926
- 'USE WHEN: the user asks "post this workbook to my Teams channel," "share this with the team in Teams," or any other outbound-file-to-Teams request. The agent has just produced or modified a workbook and wants to deliver it to a Microsoft Teams channel. ' +
927
- 'Free tier — counts against the 10k/mo cap.\n\n' +
928
- 'DO NOT USE WHEN: posting to Slack (use xlsx_post_slack). Or when no Microsoft Graph token is available — the user must have an Entra ID app registration with Group.ReadWrite.All or Files.ReadWrite.All + ChannelMessage.Send scopes, AND a valid access token for that app.',
871
+ 'Upload a local .xlsx file to a Microsoft Teams channel as a file attachment, with an optional accompanying message.\n\n' +
872
+ 'Token intake: set TEAMS_GRAPH_TOKEN in the environment (recommended keeps the token out of conversation logs). Alternatively pass graph_token as a tool argument (legacy; token will appear in MCP history). Uses Microsoft Graph\'s upload-session + chatMessage flow.\n\n' +
873
+ 'USE WHEN: the user asks "post this workbook to my Teams channel" or any outbound-file-to-Teams request after producing or modifying a workbook.\n\n' +
874
+ 'DO NOT USE WHEN: posting to Slack (use xlsx_post_slack). Or when no Microsoft Graph token is available the user needs an Entra ID app with Files.ReadWrite.All + ChannelMessage.Send scopes.',
929
875
  inputSchema: {
930
876
  type: 'object',
931
877
  properties: {
@@ -943,11 +889,10 @@ const TOOLS = [
943
889
  {
944
890
  name: 'xlsx_stamp',
945
891
  description:
946
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
947
- 'This tool: sign a workbook with a "workbook integrity verification" stamp — a cryptographic attestation embedded in docProps/custom.xml that says "this file was generated by these tools, passed these N specific checks, signed at this time, and hasn\'t been tampered with since." Factual claims only (never an opinion-shaped seal of approval). Returns the stamped workbook as base64 in _meta.file_b64; pass out_path to write to disk.\n' +
892
+ 'Sign a LOCAL .xlsx file with a "workbook integrity verification" stamp a cryptographic attestation embedded in docProps/custom.xml that says "this file was generated by these tools, passed these N specific checks, signed at this time, and hasn\'t been tampered with since." Factual claims only (never an opinion-shaped seal of approval). Returns the stamped workbook as base64 in _meta.file_b64; pass out_path to write to disk.\n\n' +
948
893
  'The caller supplies the `checks` array (e.g., from a supervisor review): list of named tests, each with passed/failed/skipped status. Verifiers see the individual check results, not a single good/bad opinion.\n\n' +
949
- 'USE WHEN: the agent has just produced or reviewed a workbook and wants to attach provable provenance + check results that travel with the file. The recipient can later verify the stamp via xlsx_verify_stamp to confirm the file hasn\'t been modified and to see the original check results.\n\n' +
950
- 'DO NOT USE WHEN: the user just wants to share a file (use xlsx_post_slack / xlsx_post_teams). Or when there are no real checks to attest to (an empty checks array is allowed and means "we ran zero checks" — but the stamp\'s value is in the checks it records).',
894
+ 'USE WHEN: an agent has just produced or reviewed a workbook and wants to attach provable provenance + check results that travel with the file. Recipients verify via xlsx_verify_stamp.\n\n' +
895
+ 'DO NOT USE WHEN: the user just wants to share a file (use xlsx_post_slack / xlsx_post_teams).',
951
896
  inputSchema: {
952
897
  type: 'object',
953
898
  properties: {
@@ -977,8 +922,7 @@ const TOOLS = [
977
922
  {
978
923
  name: 'xlsx_verify_stamp',
979
924
  description:
980
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
981
- 'This tool: verify a workbook\'s embedded integrity-verification stamp. Returns whether the cryptographic signature is valid, whether the workbook bytes match what was signed (recomputed hash vs hash IN the stamp), and the full check-result content of the stamp.\n' +
925
+ 'verify a workbook\'s embedded integrity-verification stamp. Returns whether the cryptographic signature is valid, whether the workbook bytes match what was signed (recomputed hash vs hash IN the stamp), and the full check-result content of the stamp.\n' +
982
926
  'A workbook can fail verification three ways: (1) no stamp present (file was never stamped, or the stamp was stripped); (2) signature_valid=false (someone modified the claims after signing, or signed with a different key); (3) hash_matches=false (someone modified the workbook bytes after signing). Each is a distinct trust signal.\n\n' +
983
927
  'USE WHEN: the agent (or a downstream verifier) needs to confirm a workbook hasn\'t been tampered with since it was signed, OR needs to surface the original check results that were attested to. Common scenario: incoming workbook from a counterparty, agent runs verify before trusting any of its values.',
984
928
  inputSchema: {
@@ -993,11 +937,10 @@ const TOOLS = [
993
937
  {
994
938
  name: 'xlsx_receipt',
995
939
  description:
996
- 'xlsx-for-airead, write, diff, redact, supervise .xlsx files locally.\n' +
997
- 'This tool: attach an AI-generation receipt to a workbook a cryptographic attestation embedded in docProps/custom.xml that says "this file was generated by THIS agent, at THIS time, against THESE inputs." Returns the receipted workbook as base64 in _meta.file_b64; pass out_path to write to disk.\n' +
998
- 'Honesty boundary (load-bearing): the server signs the CALLER-DECLARED `agent.name` it does NOT verify the caller actually IS that agent. The signature proves "this server signed these strings at this time," not "this came from claude-sonnet-4-6." Caller is responsible for honest declaration. Cryptographic identity binding (per-agent issued signing keys) is v1.1+ scope.\n\n' +
999
- 'USE WHEN: an AI agent (Claude, custom SDK agent, automated pipeline) generates a workbook and the recipient wants verifiable provenance "what produced this file, when, against what." Or chaining attestations across a multi-step pipeline (each step adds its own receipt under different agent.name).\n\n' +
1000
- 'DO NOT USE WHEN: the workbook was human-authored (use xlsx_stamp — Stamp attests to check results, Receipt attests to generation context). Or when the use case demands cryptographically-bound identity (v1.1+).',
940
+ 'Attach an AI-generation receipt to a LOCAL .xlsx file a cryptographic attestation embedded in docProps/custom.xml that says "this file was generated by THIS agent, at THIS time, against THESE inputs." Returns the receipted workbook as base64 in _meta.file_b64; pass out_path to write to disk.\n\n' +
941
+ 'Honesty boundary (load-bearing): the server signs the CALLER-DECLARED `agent.name` it does NOT verify the caller actually IS that agent. The signature proves "this server signed these strings at this time," not "this came from claude-sonnet-4-6." Caller is responsible for honest declaration. Cryptographic identity binding is v1.1+ scope.\n\n' +
942
+ 'USE WHEN: an AI agent generates a workbook and the recipient wants verifiable provenance "what produced this file, when, against what." Or chaining attestations across a multi-step pipeline.\n\n' +
943
+ 'DO NOT USE WHEN: the workbook was human-authored (use xlsx_stampStamp attests to check results, Receipt attests to generation context).',
1001
944
  inputSchema: {
1002
945
  type: 'object',
1003
946
  properties: {
@@ -1033,8 +976,7 @@ const TOOLS = [
1033
976
  {
1034
977
  name: 'xlsx_verify_receipt',
1035
978
  description:
1036
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
1037
- 'This tool: verify a workbook\'s embedded AI-generation receipt. Returns whether the signature is valid, whether the recomputed content hash matches the hash IN the receipt, and the full caller-declared claims (agent identity, generation timestamp, source-file hashes, prompt hash, MCP tools called, description).\n' +
979
+ 'verify a workbook\'s embedded AI-generation receipt. Returns whether the signature is valid, whether the recomputed content hash matches the hash IN the receipt, and the full caller-declared claims (agent identity, generation timestamp, source-file hashes, prompt hash, MCP tools called, description).\n' +
1038
980
  'A workbook can fail verification three ways: (1) no receipt present (never receipted, or receipt was stripped); (2) signature_valid=false (claims modified after signing); (3) hash_matches=false (workbook bytes modified after receipt was generated). Honesty: a valid receipt proves the SERVER signed the caller-DECLARED agent string — not that the agent IS that.\n\n' +
1039
981
  'USE WHEN: a workbook arrives claiming AI provenance and the user wants to verify it. Or auditing a corpus of workbooks to find ones with broken receipts (likely-tampered) or no receipts at all.',
1040
982
  inputSchema: {
@@ -1049,8 +991,7 @@ const TOOLS = [
1049
991
  {
1050
992
  name: 'xlsx_healer_diagnose',
1051
993
  description:
1052
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
1053
- 'This tool: produce a structured diagnostic report of external references that are broken or at risk in a workbook. Returns five classes of finding: (1) external-workbook references that can\'t resolve, (2) defined-name external refs, (3) Power Query connections with embedded credentials, (4) #REF! propagation maps from upstream breakage, (5) multi-hop chains (workbook → workbook → workbook). Findings carry reference_id keys that downstream cure operations key on.\n\n' +
994
+ 'produce a structured diagnostic report of external references that are broken or at risk in a workbook. Returns five classes of finding: (1) external-workbook references that can\'t resolve, (2) defined-name external refs, (3) Power Query connections with embedded credentials, (4) #REF! propagation maps from upstream breakage, (5) multi-hop chains (workbook → workbook → workbook). Findings carry reference_id keys that downstream cure operations key on.\n\n' +
1054
995
  'USE WHEN: a workbook shows #REF! errors, an agent moves a file and refs need rewriting, a customer reports "the workbook stopped working after we reorganized SharePoint", or auditing a corpus for hidden external-link breakage before sharing.\n\n' +
1055
996
  'DO NOT USE WHEN: the user wants the cleaning/normalization surface (use xlsx_data_clean — different concern). Or when there is no .xlsx source path (Healer reads the source bytes, doesn\'t reconstruct from a structured spec).',
1056
997
  inputSchema: {
@@ -1065,10 +1006,9 @@ const TOOLS = [
1065
1006
  {
1066
1007
  name: 'xlsx_healer_cure',
1067
1008
  description:
1068
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
1069
- 'This tool: apply ONE specific cure operation against a diagnosed workbook. Each operation targets a specific failure mode: rename_move (rewrite ref paths), pattern_bulk (regex-style ref rewrites), source_deleted_freeze (replace broken refs with cached values), source_deleted_redirect (point at a replacement file), source_deleted_localize (snapshot full external source into a local copy), permission_denied (strip credentials), structure_changed (rewrite formulas for moved cells), format_change (re-link after extension change), make_standalone (fully dereference all externals). Returns the cured workbook bytes + a receipt naming exactly what changed.\n\n' +
1070
- 'USE WHEN: a diagnostic report (xlsx_healer_diagnose) named a specific operation as the recommended fix and the user confirmed it; or running a known recipe across a folder of files; or restoring a workbook whose source moved by a known prefix.\n\n' +
1071
- 'DO NOT USE WHEN: the failure mode isn\'t one of the supported operations (use xlsx_healer_intent for goal-shaped fixes). Or when diagnose hasn\'t been run yet on the file (cures need diagnose-emitted reference_ids).',
1009
+ 'Apply ONE specific cure operation against a diagnosed workbook. Operations: rename_move (rewrite ref paths), pattern_bulk (regex-style ref rewrites), source_deleted_freeze (replace broken refs with cached values), source_deleted_redirect (point at a replacement file), source_deleted_localize (snapshot external source into a local copy), permission_denied (strip credentials), structure_changed (rewrite formulas for moved cells), format_change (re-link after extension change), make_standalone (fully dereference all externals). Returns cured workbook bytes + receipt.\n\n' +
1010
+ 'USE WHEN: a diagnostic report (xlsx_healer_diagnose) named a specific operation as the recommended fix; or restoring a workbook whose source moved by a known prefix.\n\n' +
1011
+ 'DO NOT USE WHEN: the failure mode isn\'t a supported operation (use xlsx_healer_intent for goal-shaped fixes). Or when diagnose hasn\'t been run (cures need diagnose-emitted reference_ids).',
1072
1012
  inputSchema: {
1073
1013
  type: 'object',
1074
1014
  properties: {
@@ -1112,8 +1052,7 @@ const TOOLS = [
1112
1052
  {
1113
1053
  name: 'xlsx_healer_simulate',
1114
1054
  description:
1115
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
1116
- 'This tool: simulate recipient-side accessibility of a workbook\'s external references. Given a list of paths the recipient CAN see (`accessible_paths`), returns which references will still resolve at the recipient end and which will break (and why). Read-only; produces no output workbook.\n\n' +
1055
+ 'simulate recipient-side accessibility of a workbook\'s external references. Given a list of paths the recipient CAN see (`accessible_paths`), returns which references will still resolve at the recipient end and which will break (and why). Read-only; produces no output workbook.\n\n' +
1117
1056
  'USE WHEN: an agent or user wants to know "will this workbook work when I send it to <person>?" before sharing — e.g., before posting to Slack, attaching to email, or sharing a OneDrive link. Or auditing a workbook against a known recipient-accessible-paths inventory.\n\n' +
1118
1057
  'DO NOT USE WHEN: the user wants to FIX the breakage (use xlsx_healer_cure or xlsx_healer_intent). Or when the recipient is the sender themselves (no path discrepancy to simulate).',
1119
1058
  inputSchema: {
@@ -1133,10 +1072,9 @@ const TOOLS = [
1133
1072
  {
1134
1073
  name: 'xlsx_healer_intent',
1135
1074
  description:
1136
- 'xlsx-for-ai read, write, diff, redact, supervise .xlsx files locally.\n' +
1137
- 'This tool: goal-driven healing. Caller declares an INTENT (`make-it-work`, `make-standalone`, or `migrate`) instead of a specific cure operation; Healer plans the operation sequence + applies it. make-it-work: minimum surgery to clear errors. make-standalone: fully de-externalize the workbook (snapshot every external dep). migrate: rewrite all references against a from/to prefix pair. Returns the planned operations, the cured bytes, and an unactionable list for refs that couldn\'t be auto-resolved.\n\n' +
1138
- 'USE WHEN: the user describes the goal in plain English ("just make this work for the recipient" / "send a fully self-contained version" / "we moved the share root, update the refs"). Or when multiple cure operations need to compose and the orchestration is non-trivial.\n\n' +
1139
- 'DO NOT USE WHEN: the user has already chosen a specific cure operation (use xlsx_healer_cure directly — avoids the planning overhead). Or when no diagnostic has been run on the workbook yet (intent uses the diagnostic surface internally; running diagnose first surfaces what intent will work with).',
1075
+ 'Goal-driven healing. Caller declares an INTENT (`make-it-work`, `make-standalone`, or `migrate`) instead of a specific cure operation; Healer plans the operation sequence + applies it. make-it-work: minimum surgery to clear errors. make-standalone: fully de-externalize (snapshot every external dep). migrate: rewrite all references against a from/to prefix pair. Returns the planned operations, cured bytes, and an unactionable list.\n\n' +
1076
+ 'USE WHEN: the user describes the goal in plain English ("just make this work for the recipient" / "send a self-contained version" / "we moved the share root, update the refs"). Or when multiple cure operations need to compose.\n\n' +
1077
+ 'DO NOT USE WHEN: the user has chosen a specific cure operation (use xlsx_healer_cure directly). Or when no diagnostic has been run on the workbook yet.',
1140
1078
  inputSchema: {
1141
1079
  type: 'object',
1142
1080
  properties: {
@@ -1168,6 +1106,68 @@ const TOOLS = [
1168
1106
  required: ['file_path', 'intent'],
1169
1107
  },
1170
1108
  },
1109
+ {
1110
+ name: 'xlsx_read_handle',
1111
+ description:
1112
+ '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' +
1113
+ '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' +
1114
+ '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.',
1115
+ inputSchema: {
1116
+ type: 'object',
1117
+ properties: {
1118
+ workbook_handle: {
1119
+ type: 'string',
1120
+ description: 'Server-side cache handle returned by the chunked-upload finalize call. 1-128 chars.',
1121
+ minLength: 1,
1122
+ maxLength: 128,
1123
+ },
1124
+ sheet: { type: 'string', description: 'Optional: restrict the read to a single sheet by name.' },
1125
+ format: {
1126
+ type: 'string',
1127
+ enum: ['md', 'json'],
1128
+ description: 'Output format. Defaults to md.',
1129
+ },
1130
+ },
1131
+ required: ['workbook_handle'],
1132
+ },
1133
+ },
1134
+ {
1135
+ name: 'xlsx_session_set_validations',
1136
+ description:
1137
+ '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' +
1138
+ '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' +
1139
+ '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).',
1140
+ inputSchema: {
1141
+ type: 'object',
1142
+ properties: {
1143
+ session_id: {
1144
+ type: 'string',
1145
+ description: 'Session identifier returned by the session-create surface. 16-128 chars.',
1146
+ minLength: 16,
1147
+ maxLength: 128,
1148
+ },
1149
+ validations: {
1150
+ type: 'array',
1151
+ 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).',
1152
+ minItems: 1,
1153
+ maxItems: 5000,
1154
+ items: {
1155
+ type: 'object',
1156
+ properties: {
1157
+ sheet: { type: 'string', description: 'Target sheet name.' },
1158
+ ref: { type: 'string', description: 'A1-style cell range the rule applies to.' },
1159
+ type: {
1160
+ type: 'string',
1161
+ description: 'Validation type. Server-side enum: whole, decimal, list, date, time, textLength, custom.',
1162
+ },
1163
+ },
1164
+ required: ['sheet', 'ref', 'type'],
1165
+ },
1166
+ },
1167
+ },
1168
+ required: ['session_id', 'validations'],
1169
+ },
1170
+ },
1171
1171
  ];
1172
1172
 
1173
1173
  // ---------------------------------------------------------------------------
@@ -1738,6 +1738,27 @@ async function dispatchTool(name, args) {
1738
1738
  return applyFileB64(result, args.out_path);
1739
1739
  }
1740
1740
 
1741
+ // Handle-based read (no file_b64; the bytes are already in the server
1742
+ // cache from a prior chunked-upload finalize). Body mirrors the server
1743
+ // schema in routes/xlsx-read-handle.ts.
1744
+ if (name === 'xlsx_read_handle') {
1745
+ const options = {};
1746
+ if (args.sheet !== undefined) options.sheet = args.sheet;
1747
+ if (args.format !== undefined) options.format = args.format;
1748
+ const body = { workbook_handle: args.workbook_handle };
1749
+ if (Object.keys(options).length > 0) body.options = options;
1750
+ return callTool('xlsx_read_handle', body);
1751
+ }
1752
+
1753
+ // Session-state write — no file bytes, just session_id + validation rules.
1754
+ // Body mirrors the server schema in routes/xlsx-session-set-validations.ts.
1755
+ if (name === 'xlsx_session_set_validations') {
1756
+ return callTool('xlsx_session_set_validations', {
1757
+ session_id: args.session_id,
1758
+ validations: args.validations,
1759
+ });
1760
+ }
1761
+
1741
1762
  // All other tools (list_sheets, schema, hyperlinks, conditional_formats,
1742
1763
  // styles, etc.) — single-file relay. Forward any common option keys the
1743
1764
  // routes accept so we don't silently drop them. New keys added here as
@@ -1758,46 +1779,56 @@ async function dispatchTool(name, args) {
1758
1779
  // ---------------------------------------------------------------------------
1759
1780
 
1760
1781
  async function main() {
1761
- await ensureRegistered();
1782
+ // Swallow EPIPE on the transport. When the client disconnects while a
1783
+ // background catalog upgrade is still in flight, sendToolListChanged
1784
+ // writes to a closed pipe and Node raises EPIPE asynchronously on the
1785
+ // Socket — our awaited try/catch around sendToolListChanged never sees
1786
+ // it. Without this guard, a client unplug after the upgrade settles
1787
+ // crashes the process with an unhandled Socket 'error' event.
1788
+ //
1789
+ // stdout is the MCP transport: EPIPE there means the client is gone,
1790
+ // exit cleanly. stderr is the log sink: an EPIPE on stderr (parent
1791
+ // closed its log pipe) is NOT a transport failure and must not take
1792
+ // the server down.
1793
+ process.stdout.on('error', (err) => {
1794
+ if (err && err.code === 'EPIPE') {
1795
+ process.exit(0);
1796
+ }
1797
+ // Anything else on the transport stream is a real failure (e.g.
1798
+ // ERR_STREAM_DESTROYED) — rethrow so it surfaces as uncaughtException
1799
+ // instead of being silently swallowed.
1800
+ throw err;
1801
+ });
1802
+ process.stderr.on('error', (err) => {
1803
+ // Silence EPIPE on stderr; rethrow anything else so we don't hide
1804
+ // genuine logging-layer bugs.
1805
+ if (!err || err.code !== 'EPIPE') throw err;
1806
+ });
1762
1807
 
1763
- // Dynamic tool catalog: query the hosted API once at startup so new
1764
- // server-side tools appear without re-publishing this npm package.
1765
- // resolveCatalog returns the baked-in TOOLS as last-resort fallback so
1766
- // we never fail-open on a transient network blip. See lib/discover.js.
1808
+ // `initialize` MUST respond from local state never block on the network.
1809
+ // Under Claude Desktop's bundled Node 24.x runtime, the registration POST
1810
+ // and the catalog GET can hang indefinitely (Happy-Eyeballs / IPv6 dial
1811
+ // edge cases inside Electron), and the client gives up at 60s. The whole
1812
+ // MCP attach dies before tools/list is even called.
1767
1813
  //
1768
- // Hard timeout (8s) on top of any timeout inside resolveCatalog so that
1769
- // a network call which hangs forever (DNS sinkhole, TCP black hole, slow-
1770
- // loris-style stalled response) cannot block MCP server startup
1771
- // indefinitely. Pre-Friday-external CRITICAL per the Tier-1 audit.
1772
- const CATALOG_FETCH_TIMEOUT_MS = 8000;
1773
- let catalog;
1774
- try {
1775
- catalog = await Promise.race([
1776
- resolveCatalog(TOOLS),
1777
- new Promise((_, reject) =>
1778
- setTimeout(
1779
- () => reject(new Error(`catalog fetch timed out after ${CATALOG_FETCH_TIMEOUT_MS}ms`)),
1780
- CATALOG_FETCH_TIMEOUT_MS
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 : []);
1814
+ // Shape: connect transport FIRST with the bundled TOOLS as the floor.
1815
+ // Then background-upgrade registration + catalog with bounded timeouts,
1816
+ // and fire notifications/tools/list_changed once the live catalog lands.
1817
+ // The bundled set already covers every tool the user reaches in normal
1818
+ // flows; the upgrade is additive.
1819
+ // sanitizeForMcp guarantees every tool the server emits has a valid
1820
+ // inputSchema + description — without it Claude Desktop silently drops
1821
+ // tools that lack inputSchema, which is the exact symptom in SPM P0
1822
+ // 2026-06-05 (mcp-toolslist-missing-inputschema). For the bundled
1823
+ // catalog this is a no-op (every TOOLS entry already has full fields);
1824
+ // for the upgraded catalog it's the floor that keeps stub server
1825
+ // entries registerable.
1826
+ let liveTools = sanitizeForMcp(applyAnnotations(TOOLS));
1827
+ process.stderr.write(`xlsx-for-ai-mcp: tool catalog source=bundled count=${liveTools.length}\n`);
1797
1828
 
1798
1829
  const server = new Server(
1799
1830
  { name: 'xlsx-for-ai', version: require('./package.json').version },
1800
- { capabilities: { tools: {} } }
1831
+ { capabilities: { tools: { listChanged: true } } }
1801
1832
  );
1802
1833
 
1803
1834
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: liveTools }));
@@ -1841,6 +1872,88 @@ async function main() {
1841
1872
 
1842
1873
  const transport = new StdioServerTransport();
1843
1874
  await server.connect(transport);
1875
+
1876
+ // Background-upgrade: registration + dynamic catalog. Bounded so a
1877
+ // hung network never wastes resources; failure is non-fatal because
1878
+ // the bundled catalog already serves tools/list. Detached on purpose
1879
+ // — we do not await this; main() returns and the upgrade lands when
1880
+ // it lands.
1881
+ upgradeCatalogInBackground(server, (next) => {
1882
+ liveTools = next;
1883
+ });
1884
+ }
1885
+
1886
+ async function withTimeout(promise, ms, label) {
1887
+ // Promise.race with a setTimeout-rejecting promise leaks unhandled
1888
+ // rejections in two directions:
1889
+ // (a) Main wins — the timer still fires later and its branch
1890
+ // rejects with nobody awaiting it. clearTimeout in finally
1891
+ // eliminates this.
1892
+ // (b) Timer wins — the original promise can still reject later
1893
+ // (the underlying fetch eventually errors out long after we
1894
+ // gave up). Attaching a no-op catch ensures that late
1895
+ // rejection is consumed instead of crashing the MCP server
1896
+ // minutes after startup.
1897
+ // The (b) case is the SPM P0 surface: the bundled-Node-24 dial
1898
+ // can stall, time out, and then much later reject with EAI_AGAIN
1899
+ // or a TLS error — by then nobody is listening.
1900
+ promise.catch(() => {});
1901
+ let timer;
1902
+ try {
1903
+ return await Promise.race([
1904
+ promise,
1905
+ new Promise((_, reject) => {
1906
+ timer = setTimeout(
1907
+ () => reject(new Error(`${label} timed out after ${ms}ms`)),
1908
+ ms
1909
+ );
1910
+ }),
1911
+ ]);
1912
+ } finally {
1913
+ if (timer) clearTimeout(timer);
1914
+ }
1915
+ }
1916
+
1917
+ async function upgradeCatalogInBackground(server, swap) {
1918
+ const REGISTRATION_TIMEOUT_MS = 10_000;
1919
+ const CATALOG_TIMEOUT_MS = 8_000;
1920
+
1921
+ try {
1922
+ await withTimeout(ensureRegistered(), REGISTRATION_TIMEOUT_MS, 'registration');
1923
+ } catch (err) {
1924
+ process.stderr.write(`xlsx-for-ai-mcp: registration deferred (${err.message})\n`);
1925
+ }
1926
+
1927
+ let catalog;
1928
+ try {
1929
+ catalog = await withTimeout(resolveCatalog(TOOLS), CATALOG_TIMEOUT_MS, 'catalog fetch');
1930
+ } catch (err) {
1931
+ process.stderr.write(`xlsx-for-ai-mcp: catalog upgrade skipped (${err.message})\n`);
1932
+ return;
1933
+ }
1934
+
1935
+ if (!catalog || !Array.isArray(catalog.tools)) {
1936
+ return;
1937
+ }
1938
+ // No upgrade to apply when discover.js fell back to the baked-in set
1939
+ // (source=static): the list is identical to what initialize already
1940
+ // returned, so a list_changed notification would be wire noise.
1941
+ if (catalog.source === 'static') {
1942
+ process.stderr.write(`xlsx-for-ai-mcp: catalog upgrade unavailable (source=static) — staying on bundled\n`);
1943
+ return;
1944
+ }
1945
+
1946
+ const upgraded = sanitizeForMcp(applyAnnotations(catalog.tools));
1947
+ swap(upgraded);
1948
+ process.stderr.write(`xlsx-for-ai-mcp: tool catalog source=${catalog.source} count=${upgraded.length}\n`);
1949
+
1950
+ try {
1951
+ await server.sendToolListChanged();
1952
+ } catch (_) {
1953
+ // Transport may already be torn down (client disconnected before the
1954
+ // upgrade landed). Non-fatal — next attach starts with the bundled
1955
+ // catalog and retries the upgrade.
1956
+ }
1844
1957
  }
1845
1958
 
1846
1959
  // 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.0",
4
+ "version": "3.0.4",
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": {