xlsx-for-ai 2.26.0 → 3.0.0

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,13 +1,17 @@
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
+
3
5
  **The missing reliability layer that makes spreadsheet reasoning production-grade for LLMs.**
4
6
 
5
- A thin npm client over a hosted API. Install once, add to your agent config, and your agent gets six production-grade tools for reading, writing, diffing, and redacting `.xlsx` files — engine complexity runs server-side, engine IP stays private.
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.
6
8
 
7
9
  ```bash
8
10
  npm install -g xlsx-for-ai
9
11
  ```
10
12
 
13
+ **Or — recommended for MCP use:** the canonical configs below use `npx -y xlsx-for-ai@latest`, which fetches and runs the latest version on every client restart. Self-heals across releases without a manual global re-install when a new version ships.
14
+
11
15
  > **Upgrading from 1.5.x?** This is a re-architecture, not a feature bump: the heavy local engine is gone from the npm package. All rendering happens server-side. The `cursor-reads-xlsx` alias still works. See [Migration](#migration-from-15x) below.
12
16
 
13
17
  ---
@@ -30,13 +34,14 @@ The bundle includes the full npm package and registers all MCP tools automatical
30
34
  {
31
35
  "mcpServers": {
32
36
  "xlsx-for-ai": {
33
- "command": "xlsx-for-ai-mcp"
37
+ "command": "npx",
38
+ "args": ["-y", "-p", "xlsx-for-ai@latest", "xlsx-for-ai-mcp"]
34
39
  }
35
40
  }
36
41
  }
37
42
  ```
38
43
 
39
- Verify either path: restart Claude Desktop, open a new conversation, and ask "what MCP tools do you have?" — 37 `xlsx_*` tools should appear, including `xlsx_doctor` (one-call health report — try it first on any unknown workbook).
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).
40
45
 
41
46
  ### Cursor
42
47
 
@@ -46,7 +51,8 @@ Config file: `~/.cursor/mcp.json`
46
51
  {
47
52
  "mcpServers": {
48
53
  "xlsx-for-ai": {
49
- "command": "xlsx-for-ai-mcp"
54
+ "command": "npx",
55
+ "args": ["-y", "-p", "xlsx-for-ai@latest", "xlsx-for-ai-mcp"]
50
56
  }
51
57
  }
52
58
  }
@@ -63,7 +69,8 @@ Config file: `~/.continue/config.json`
63
69
  "mcpServers": [
64
70
  {
65
71
  "name": "xlsx-for-ai",
66
- "command": "xlsx-for-ai-mcp"
72
+ "command": "npx",
73
+ "args": ["-y", "-p", "xlsx-for-ai@latest", "xlsx-for-ai-mcp"]
67
74
  }
68
75
  ]
69
76
  }
@@ -79,7 +86,8 @@ Pass `--mcp-server` on the command line, or add to your Codex config:
79
86
  {
80
87
  "mcpServers": {
81
88
  "xlsx-for-ai": {
82
- "command": "xlsx-for-ai-mcp"
89
+ "command": "npx",
90
+ "args": ["-y", "-p", "xlsx-for-ai@latest", "xlsx-for-ai-mcp"]
83
91
  }
84
92
  }
85
93
  }
@@ -96,8 +104,8 @@ Config file: `~/.config/zed/settings.json`
96
104
  "context_servers": {
97
105
  "xlsx-for-ai": {
98
106
  "command": {
99
- "path": "xlsx-for-ai-mcp",
100
- "args": []
107
+ "path": "npx",
108
+ "args": ["-y", "-p", "xlsx-for-ai@latest", "xlsx-for-ai-mcp"]
101
109
  }
102
110
  }
103
111
  }
@@ -114,7 +122,8 @@ Config file: `~/.codeium/windsurf/mcp_config.json`
114
122
  {
115
123
  "mcpServers": {
116
124
  "xlsx-for-ai": {
117
- "command": "xlsx-for-ai-mcp"
125
+ "command": "npx",
126
+ "args": ["-y", "-p", "xlsx-for-ai@latest", "xlsx-for-ai-mcp"]
118
127
  }
119
128
  }
120
129
  }
@@ -130,7 +139,7 @@ For custom MCP clients, the binary is `xlsx-for-ai-mcp` (stdio transport). Overr
130
139
 
131
140
  ## What it does
132
141
 
133
- 44 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).
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).
134
143
 
135
144
  ### Triage / orient
136
145
 
package/index.js CHANGED
@@ -218,21 +218,30 @@ function loadChecksFile(checksPath) {
218
218
  // ---------------------------------------------------------------------------
219
219
  // Heal subcommand — exposes the healer-deep HTTP routes from the CLI.
220
220
  //
221
- // xlsx-for-ai heal <path> diagnose-only (default)
222
- // xlsx-for-ai heal <path> --diagnose-only explicit form of the default
223
- // xlsx-for-ai heal <path> --operation <op> --params <json> [--mode <m>] [--out <path>]
224
- // xlsx-for-ai heal <path> --format text|json [other flags]
221
+ // xlsx-for-ai heal <path> diagnose-only (default)
222
+ // xlsx-for-ai heal <path> --diagnose-only explicit form of the default
223
+ // xlsx-for-ai heal <path> --operation <op> --params <json> apply one cure operation
224
+ // xlsx-for-ai heal <path> --intent <make-it-work|make-standalone|migrate>
225
+ // [--from <prefix>] [--to <prefix>] intent-driven cure (plan + apply)
226
+ // [--confirm] required for --mode in_place
227
+ // xlsx-for-ai heal <path> [--mode as_copy|in_place] [--out <path>] [--format text|json]
225
228
  //
226
- // `--intent`/`--from`/`--to` are reserved for v1.1 once the
227
- // /api/v1/tools/xlsx_healer_intent route ships; the CLI rejects
228
- // those flags today so callers see a clean "v1.1" message rather
229
- // than a server 404.
229
+ // The intent path requires the /api/v1/tools/xlsx_healer_intent route
230
+ // to be live; it's been deployed since 2026-06-04. `--from` and `--to`
231
+ // are required when intent=migrate (the route also enforces this); for
232
+ // other intents they're ignored.
230
233
  // ---------------------------------------------------------------------------
231
234
 
235
+ const VALID_INTENTS = new Set(['make-it-work', 'make-standalone', 'migrate']);
236
+
232
237
  async function runHealSubcommand(rest) {
233
238
  if (rest.length === 0 || rest[0].startsWith('-')) {
234
239
  process.stderr.write(
235
- 'Usage: xlsx-for-ai heal <file.xlsx> [--diagnose-only | --operation <op> --params <json>] [--mode as_copy|in_place] [--out <path>] [--format text|json]\n',
240
+ 'Usage: xlsx-for-ai heal <file.xlsx>\n' +
241
+ ' [--diagnose-only]\n' +
242
+ ' [--operation <op> --params <json>]\n' +
243
+ ' [--intent <make-it-work|make-standalone|migrate> [--from <prefix>] [--to <prefix>] [--confirm]]\n' +
244
+ ' [--mode as_copy|in_place] [--out <path>] [--format text|json]\n',
236
245
  );
237
246
  process.exit(2);
238
247
  }
@@ -246,6 +255,10 @@ async function runHealSubcommand(rest) {
246
255
  let diagnoseOnly = false;
247
256
  let operation = null;
248
257
  let paramsJson = null;
258
+ let intent = null;
259
+ let intentFrom = null;
260
+ let intentTo = null;
261
+ let confirm = false;
249
262
  let mode = 'as_copy';
250
263
  let outPath = null;
251
264
  let format = 'text';
@@ -254,13 +267,14 @@ async function runHealSubcommand(rest) {
254
267
  if (a === '--diagnose-only') diagnoseOnly = true;
255
268
  else if (a === '--operation') operation = nextRequiredArg(rest, i++, '--operation');
256
269
  else if (a === '--params') paramsJson = nextRequiredArg(rest, i++, '--params');
270
+ else if (a === '--intent') intent = nextRequiredArg(rest, i++, '--intent');
271
+ else if (a === '--from') intentFrom = nextRequiredArg(rest, i++, '--from');
272
+ else if (a === '--to') intentTo = nextRequiredArg(rest, i++, '--to');
273
+ else if (a === '--confirm') confirm = true;
257
274
  else if (a === '--mode') mode = nextRequiredArg(rest, i++, '--mode');
258
275
  else if (a === '--out') outPath = nextRequiredArg(rest, i++, '--out');
259
276
  else if (a === '--format') format = nextRequiredArg(rest, i++, '--format');
260
- else if (a === '--intent' || a === '--from' || a === '--to') {
261
- process.stderr.write(`xlsx-for-ai heal: ${a} is reserved for v1.1 (intent-driven cures via /xlsx_healer_intent); not yet wired\n`);
262
- process.exit(2);
263
- } else {
277
+ else {
264
278
  process.stderr.write(`Unknown flag: ${a}\n`);
265
279
  process.exit(2);
266
280
  }
@@ -268,19 +282,43 @@ async function runHealSubcommand(rest) {
268
282
 
269
283
  // Validate mutually-exclusive shapes early — clearer than letting
270
284
  // the server emit `conflicting_repair_directives` on its O8 check.
271
- if (diagnoseOnly && operation) {
272
- process.stderr.write('xlsx-for-ai heal: --diagnose-only and --operation are mutually exclusive\n');
285
+ const modeCount = (diagnoseOnly ? 1 : 0) + (operation ? 1 : 0) + (intent ? 1 : 0);
286
+ if (modeCount > 1) {
287
+ process.stderr.write(
288
+ 'xlsx-for-ai heal: --diagnose-only, --operation, and --intent are mutually exclusive — pick one.\n',
289
+ );
273
290
  process.exit(2);
274
291
  }
275
- if (!diagnoseOnly && !operation) {
292
+ if (modeCount === 0) {
276
293
  // Default to diagnose-only — first-touch use of `xlsx-for-ai heal`
277
294
  // should show the user what's wrong before they pick a cure.
278
295
  diagnoseOnly = true;
279
296
  }
297
+ if (intent !== null && !VALID_INTENTS.has(intent)) {
298
+ process.stderr.write(
299
+ `xlsx-for-ai heal: --intent must be one of: ${[...VALID_INTENTS].join(', ')} (got '${intent}')\n`,
300
+ );
301
+ process.exit(2);
302
+ }
303
+ if (intent === 'migrate' && (!intentFrom || !intentTo)) {
304
+ process.stderr.write(
305
+ 'xlsx-for-ai heal: --intent migrate requires both --from <prefix> and --to <prefix>\n',
306
+ );
307
+ process.exit(2);
308
+ }
280
309
  if (mode !== 'as_copy' && mode !== 'in_place') {
281
310
  process.stderr.write(`xlsx-for-ai heal: --mode must be 'as_copy' or 'in_place' (got '${mode}')\n`);
282
311
  process.exit(2);
283
312
  }
313
+ if (mode === 'in_place' && !confirm && (operation || intent)) {
314
+ // Server-side intent route enforces confirm for in_place — surface
315
+ // the same gate client-side so the CLI's error message matches the
316
+ // intent of the safety check (don't overwrite without confirmation).
317
+ process.stderr.write(
318
+ 'xlsx-for-ai heal: --mode in_place requires --confirm (explicit overwrite gate)\n',
319
+ );
320
+ process.exit(2);
321
+ }
284
322
  if (format !== 'text' && format !== 'json') {
285
323
  process.stderr.write(`xlsx-for-ai heal: --format must be 'text' or 'json' (got '${format}')\n`);
286
324
  process.exit(2);
@@ -316,31 +354,52 @@ async function runHealSubcommand(rest) {
316
354
  return 0;
317
355
  }
318
356
 
319
- // ---- cure path ---------------------------------------------------------
320
- let cureParams = {};
321
- if (paramsJson !== null) {
357
+ // ---- cure or intent path -----------------------------------------------
358
+ let result;
359
+ let healLabel; // for the friendly error prefix
360
+ if (intent) {
361
+ // Intent path — server plans + applies. intent_params carries the
362
+ // optional from/to prefix pair for the migrate intent.
363
+ const intentParams = {};
364
+ if (intentFrom) intentParams.from = intentFrom;
365
+ if (intentTo) intentParams.to = intentTo;
366
+ const body = { file_b64: fileB64, intent, mode };
367
+ if (Object.keys(intentParams).length > 0) body.intent_params = intentParams;
368
+ if (mode === 'in_place') body.confirm = true;
369
+ healLabel = `--intent ${intent}`;
322
370
  try {
323
- cureParams = JSON.parse(paramsJson);
324
- } catch (e) {
325
- process.stderr.write(`xlsx-for-ai heal: --params is not valid JSON: ${e.message}\n`);
326
- process.exit(2);
371
+ result = await callTool('xlsx_healer_intent', body);
372
+ } catch (err) {
373
+ process.stderr.write(friendlyCliError(`xlsx-for-ai heal ${healLabel}`, err) + '\n');
374
+ process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
327
375
  }
328
- if (cureParams === null || typeof cureParams !== 'object' || Array.isArray(cureParams)) {
329
- process.stderr.write('xlsx-for-ai heal: --params must be a JSON object\n');
330
- process.exit(2);
376
+ } else {
377
+ let cureParams = {};
378
+ if (paramsJson !== null) {
379
+ try {
380
+ cureParams = JSON.parse(paramsJson);
381
+ } catch (e) {
382
+ process.stderr.write(`xlsx-for-ai heal: --params is not valid JSON: ${e.message}\n`);
383
+ process.exit(2);
384
+ }
385
+ if (cureParams === null || typeof cureParams !== 'object' || Array.isArray(cureParams)) {
386
+ process.stderr.write('xlsx-for-ai heal: --params must be a JSON object\n');
387
+ process.exit(2);
388
+ }
331
389
  }
332
- }
333
390
 
334
- const body = { file_b64: fileB64, operation, cure_params: cureParams, mode };
335
- let result;
336
- try {
337
- result = await callTool('xlsx_healer_cure', body);
338
- } catch (err) {
339
- // Same sanitization shape as the diagnose path — friendlyCliError
340
- // (above) maps known codes to canned messages; raw err.message
341
- // only surfaces with XFA_DEBUG=1 for incident triage.
342
- process.stderr.write(friendlyCliError(`xlsx-for-ai heal --operation ${operation}`, err) + '\n');
343
- process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
391
+ const body = { file_b64: fileB64, operation, cure_params: cureParams, mode };
392
+ if (mode === 'in_place') body.confirm = true;
393
+ healLabel = `--operation ${operation}`;
394
+ try {
395
+ result = await callTool('xlsx_healer_cure', body);
396
+ } catch (err) {
397
+ // Same sanitization shape as the diagnose path friendlyCliError
398
+ // (above) maps known codes to canned messages; raw err.message
399
+ // only surfaces with XFA_DEBUG=1 for incident triage.
400
+ process.stderr.write(friendlyCliError(`xlsx-for-ai heal ${healLabel}`, err) + '\n');
401
+ process.exit(err.code === 'API_UNREACHABLE' || err.code === 'API_SERVER_ERROR' ? 3 : 1);
402
+ }
344
403
  }
345
404
 
346
405
  const meta = (result && result._meta) || {};
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MCP tool annotations — canonical source.
5
+ *
6
+ * Per MCP spec (2025-06-18+) tool annotations describe runtime behavior:
7
+ * - title Human-readable tool name
8
+ * - readOnlyHint Tool does NOT modify its environment
9
+ * - destructiveHint Tool may perform irreversible side-effects
10
+ *
11
+ * The annotations live here rather than inline on each tool definition so:
12
+ * 1. They overlay onto tools regardless of source — static fallback,
13
+ * cached catalog, or freshly-fetched remote. The remote /api/v1/tools/list
14
+ * currently returns minimal entries; this overlay restores the
15
+ * annotations the wire format would otherwise drop.
16
+ * 2. They drive manifest generation downstream (MCPB, M365 declarative
17
+ * agent, future OpenAPI). One annotation change → all manifests
18
+ * regenerate consistently.
19
+ *
20
+ * Classification rules:
21
+ * - readOnlyHint: true → tool only reads; never writes a file or causes
22
+ * an externally observable side-effect.
23
+ * - destructiveHint: true → tool causes an irreversible external action
24
+ * (e.g., posts to a third-party system).
25
+ * Note: tools that write a NEW file (Save-As shape) are NOT destructive
26
+ * even though readOnlyHint is false — the source workbook is preserved.
27
+ * destructiveHint is reserved for actions that cannot be undone by
28
+ * deleting the output, which means external side-effects.
29
+ */
30
+
31
+ const TOOL_ANNOTATIONS = Object.freeze({
32
+ // ---- Reading / inspection: 35 read-only tools -------------------------
33
+ xlsx_read: { title: 'Read Excel file', readOnlyHint: true, destructiveHint: false },
34
+ xlsx_list_sheets: { title: 'List Excel sheets', readOnlyHint: true, destructiveHint: false },
35
+ xlsx_schema: { title: 'Infer Excel column types', readOnlyHint: true, destructiveHint: false },
36
+ xlsx_diff: { title: 'Diff two Excel workbooks', readOnlyHint: true, destructiveHint: false },
37
+ xlsx_describe: { title: 'Summarize Excel columns', readOnlyHint: true, destructiveHint: false },
38
+ xlsx_filter: { title: 'Filter Excel rows', readOnlyHint: true, destructiveHint: false },
39
+ xlsx_aggregate: { title: 'Group-by aggregate Excel rows', readOnlyHint: true, destructiveHint: false },
40
+ xlsx_named_ranges: { title: 'List Excel named ranges', readOnlyHint: true, destructiveHint: false },
41
+ xlsx_sort: { title: 'Sort Excel rows', readOnlyHint: true, destructiveHint: false },
42
+ xlsx_value_counts: { title: 'Count Excel column values', readOnlyHint: true, destructiveHint: false },
43
+ xlsx_formulas: { title: 'Inspect Excel formulas', readOnlyHint: true, destructiveHint: false },
44
+ xlsx_tables: { title: 'List Excel tables', readOnlyHint: true, destructiveHint: false },
45
+ xlsx_pivot: { title: 'Pivot Excel data', readOnlyHint: true, destructiveHint: false },
46
+ xlsx_eval: { title: 'Evaluate Excel formula', readOnlyHint: true, destructiveHint: false },
47
+ xlsx_validate: { title: 'Cross-engine validate Excel', readOnlyHint: true, destructiveHint: false },
48
+ xlsx_data_validations: { title: 'List Excel data-validation rules', readOnlyHint: true, destructiveHint: false },
49
+ xlsx_hyperlinks: { title: 'List Excel hyperlinks', readOnlyHint: true, destructiveHint: false },
50
+ xlsx_topology: { title: 'Map Excel sheet topology', readOnlyHint: true, destructiveHint: false },
51
+ xlsx_conditional_formats: { title: 'List Excel conditional formats', readOnlyHint: true, destructiveHint: false },
52
+ xlsx_comments: { title: 'List Excel comments', readOnlyHint: true, destructiveHint: false },
53
+ xlsx_doctor: { title: 'Audit Excel workbook health', readOnlyHint: true, destructiveHint: false },
54
+ xlsx_form_controls: { title: 'List Excel form controls', readOnlyHint: true, destructiveHint: false },
55
+ xlsx_macros: { title: 'List Excel VBA macros', readOnlyHint: true, destructiveHint: false },
56
+ xlsx_merged_cells: { title: 'List Excel merged cells', readOnlyHint: true, destructiveHint: false },
57
+ xlsx_workbook_views: { title: 'List Excel workbook views', readOnlyHint: true, destructiveHint: false },
58
+ xlsx_print_settings: { title: 'List Excel print settings', readOnlyHint: true, destructiveHint: false },
59
+ xlsx_properties: { title: 'Read Excel document properties', readOnlyHint: true, destructiveHint: false },
60
+ xlsx_external_links: { title: 'List Excel external links', readOnlyHint: true, destructiveHint: false },
61
+ xlsx_slicers_timelines: { title: 'List Excel slicers and timelines', readOnlyHint: true, destructiveHint: false },
62
+ xlsx_pivot_tables: { title: 'List Excel pivot tables', readOnlyHint: true, destructiveHint: false },
63
+ xlsx_images: { title: 'List Excel embedded images', readOnlyHint: true, destructiveHint: false },
64
+ xlsx_charts: { title: 'List Excel charts', readOnlyHint: true, destructiveHint: false },
65
+ xlsx_protection: { title: 'List Excel protection settings', readOnlyHint: true, destructiveHint: false },
66
+ xlsx_styles: { title: 'List Excel cell styles', readOnlyHint: true, destructiveHint: false },
67
+ xlsx_verify_stamp: { title: 'Verify Excel integrity stamp', readOnlyHint: true, destructiveHint: false },
68
+
69
+ // ---- Writing — non-destructive: 5 Save-As-shape tools -----------------
70
+ // Source workbook is preserved; output goes to a new path or returned bytes.
71
+ xlsx_write: { title: 'Write Excel file', readOnlyHint: false, destructiveHint: false },
72
+ xlsx_redact: { title: 'Redact Excel file', readOnlyHint: false, destructiveHint: false },
73
+ xlsx_convert: { title: 'Convert Excel to other format', readOnlyHint: false, destructiveHint: false },
74
+ xlsx_data_clean: { title: 'Clean Excel data', readOnlyHint: false, destructiveHint: false },
75
+ xlsx_stamp: { title: 'Stamp Excel with integrity verification', readOnlyHint: false, destructiveHint: false },
76
+
77
+ // ---- External side-effects — destructive: 2 tools ---------------------
78
+ // A post can't be undone; the message lands in a third-party system.
79
+ xlsx_post_slack: { title: 'Post Excel summary to Slack', readOnlyHint: false, destructiveHint: true },
80
+ xlsx_post_teams: { title: 'Post Excel summary to Teams', readOnlyHint: false, destructiveHint: true },
81
+ });
82
+
83
+ /**
84
+ * Overlay annotations onto an MCP-shaped tool array.
85
+ *
86
+ * Returns a new array with each tool extended with an `annotations` object
87
+ * pulled from TOOL_ANNOTATIONS by name. Tools without a known annotation
88
+ * pass through unchanged — this is intentional so that a dynamically-
89
+ * discovered tool the client doesn't recognize still appears, just without
90
+ * the annotation hints. The annotation map should be updated whenever
91
+ * a new tool is added to the server.
92
+ */
93
+ // Keys we refuse to copy from upstream annotation objects — guards against
94
+ // prototype-pollution if a remote /api/v1/tools/list ever returns hostile
95
+ // data. Our overlay map is a frozen const so it's safe to spread directly;
96
+ // the danger is only the foreign `existing` object.
97
+ const POLLUTION_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
98
+
99
+ function applyAnnotations(tools) {
100
+ if (!Array.isArray(tools)) return tools;
101
+ return tools.map((t) => {
102
+ if (!t || typeof t.name !== 'string') return t;
103
+ const ann = TOOL_ANNOTATIONS[t.name];
104
+ if (!ann) return t;
105
+ // Preserve any annotations the upstream source already carries; ours fill
106
+ // in the gaps without clobbering richer remote data. Reject pollution
107
+ // keys and non-plain-object inputs.
108
+ const merged = { ...ann };
109
+ if (t.annotations && typeof t.annotations === 'object' && !Array.isArray(t.annotations)) {
110
+ for (const [k, v] of Object.entries(t.annotations)) {
111
+ if (POLLUTION_KEYS.has(k)) continue;
112
+ merged[k] = v;
113
+ }
114
+ }
115
+ return { ...t, annotations: merged };
116
+ });
117
+ }
118
+
119
+ module.exports = {
120
+ TOOL_ANNOTATIONS,
121
+ applyAnnotations,
122
+ };
package/mcp.js CHANGED
@@ -1045,6 +1045,129 @@ const TOOLS = [
1045
1045
  required: ['file_path'],
1046
1046
  },
1047
1047
  },
1048
+
1049
+ {
1050
+ name: 'xlsx_healer_diagnose',
1051
+ 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' +
1054
+ '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
+ '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
+ inputSchema: {
1057
+ type: 'object',
1058
+ properties: {
1059
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file to diagnose.' },
1060
+ },
1061
+ required: ['file_path'],
1062
+ },
1063
+ },
1064
+
1065
+ {
1066
+ name: 'xlsx_healer_cure',
1067
+ 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).',
1072
+ inputSchema: {
1073
+ type: 'object',
1074
+ properties: {
1075
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file to cure.' },
1076
+ operation: {
1077
+ type: 'string',
1078
+ description: 'The cure operation to apply.',
1079
+ enum: [
1080
+ 'rename_move',
1081
+ 'pattern_bulk',
1082
+ 'source_deleted_freeze',
1083
+ 'source_deleted_redirect',
1084
+ 'source_deleted_localize',
1085
+ 'permission_denied',
1086
+ 'structure_changed',
1087
+ 'format_change',
1088
+ 'make_standalone',
1089
+ 'chain_collapse',
1090
+ 'modernize_to_pq',
1091
+ ],
1092
+ },
1093
+ cure_params: {
1094
+ type: 'object',
1095
+ description: 'Operation-specific parameters. E.g., rename_move takes {from_prefix, to_prefix}; pattern_bulk takes {pattern, replacement}.',
1096
+ },
1097
+ mode: {
1098
+ type: 'string',
1099
+ enum: ['as_copy', 'in_place'],
1100
+ description: 'as_copy (default) writes a new file alongside the source; in_place overwrites it.',
1101
+ },
1102
+ confirm: {
1103
+ type: 'boolean',
1104
+ description: 'Required as true when mode=in_place. Prevents accidental in-place overwrites; explicit confirmation is the safety gate.',
1105
+ },
1106
+ out_path: { type: 'string', description: 'Optional: write the cured workbook to this absolute path. Defaults to <name>-healed.xlsx next to the source when mode=as_copy.' },
1107
+ },
1108
+ required: ['file_path', 'operation'],
1109
+ },
1110
+ },
1111
+
1112
+ {
1113
+ name: 'xlsx_healer_simulate',
1114
+ 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' +
1117
+ '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
+ '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
+ inputSchema: {
1120
+ type: 'object',
1121
+ properties: {
1122
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file to simulate.' },
1123
+ accessible_paths: {
1124
+ type: 'array',
1125
+ items: { type: 'string' },
1126
+ description: 'List of paths (absolute or URL) the recipient CAN see. Max 1000 entries, each ≤4096 chars. Often a folder tree or a SharePoint root.',
1127
+ },
1128
+ },
1129
+ required: ['file_path', 'accessible_paths'],
1130
+ },
1131
+ },
1132
+
1133
+ {
1134
+ name: 'xlsx_healer_intent',
1135
+ 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).',
1140
+ inputSchema: {
1141
+ type: 'object',
1142
+ properties: {
1143
+ file_path: { type: 'string', description: 'Absolute path to the .xlsx file to heal.' },
1144
+ intent: {
1145
+ type: 'string',
1146
+ enum: ['make-it-work', 'make-standalone', 'migrate'],
1147
+ description: 'The healing goal. make-it-work: smallest surgery to clear errors. make-standalone: fully dereference all externals. migrate: rewrite against a from/to prefix pair (requires intent_params.from + intent_params.to).',
1148
+ },
1149
+ intent_params: {
1150
+ type: 'object',
1151
+ properties: {
1152
+ from: { type: 'string', description: 'Source path prefix (required for migrate intent).' },
1153
+ to: { type: 'string', description: 'Target path prefix (required for migrate intent).' },
1154
+ },
1155
+ description: 'Intent-specific parameters. Required keys depend on the intent (migrate needs from + to).',
1156
+ },
1157
+ mode: {
1158
+ type: 'string',
1159
+ enum: ['as_copy', 'in_place'],
1160
+ description: 'as_copy (default) writes a new file alongside the source; in_place overwrites it.',
1161
+ },
1162
+ confirm: {
1163
+ type: 'boolean',
1164
+ description: 'Required as true when mode=in_place. Prevents accidental in-place overwrites; explicit confirmation is the safety gate.',
1165
+ },
1166
+ out_path: { type: 'string', description: 'Optional: write the cured workbook to this absolute path. Defaults to <name>-healed.xlsx next to the source when mode=as_copy.' },
1167
+ },
1168
+ required: ['file_path', 'intent'],
1169
+ },
1170
+ },
1048
1171
  ];
1049
1172
 
1050
1173
  // ---------------------------------------------------------------------------
@@ -1567,6 +1690,54 @@ async function dispatchTool(name, args) {
1567
1690
  return callTool('xlsx_verify_receipt', body);
1568
1691
  }
1569
1692
 
1693
+ // xlsx_healer_diagnose: produce structured diagnostic report of broken/
1694
+ // at-risk external refs. Read-only; returns the report in the response
1695
+ // _meta block. No output file.
1696
+ if (name === 'xlsx_healer_diagnose') {
1697
+ const body = { file_b64: fileToB64(args.file_path) };
1698
+ return callTool('xlsx_healer_diagnose', body);
1699
+ }
1700
+
1701
+ // xlsx_healer_cure: apply ONE specific cure operation. Returns the
1702
+ // cured bytes in _meta.file_b64 + a per-operation receipt; out_path
1703
+ // (or in_place mode) triggers the standard applyFileB64 disk write.
1704
+ if (name === 'xlsx_healer_cure') {
1705
+ const body = {
1706
+ file_b64: fileToB64(args.file_path),
1707
+ operation: args.operation,
1708
+ };
1709
+ if (args.cure_params !== undefined) body.cure_params = args.cure_params;
1710
+ if (args.mode !== undefined) body.mode = args.mode;
1711
+ if (args.confirm !== undefined) body.confirm = args.confirm;
1712
+ const result = await callTool('xlsx_healer_cure', body);
1713
+ return applyFileB64(result, args.out_path);
1714
+ }
1715
+
1716
+ // xlsx_healer_simulate: recipient-side accessibility check. Read-only;
1717
+ // returns the simulation report in _meta. No output file.
1718
+ if (name === 'xlsx_healer_simulate') {
1719
+ const body = {
1720
+ file_b64: fileToB64(args.file_path),
1721
+ accessible_paths: args.accessible_paths,
1722
+ };
1723
+ return callTool('xlsx_healer_simulate', body);
1724
+ }
1725
+
1726
+ // xlsx_healer_intent: goal-driven healing (plan + apply). Returns the
1727
+ // planned operations + cured bytes + unactionable list. Same out_path /
1728
+ // in_place semantics as xlsx_healer_cure.
1729
+ if (name === 'xlsx_healer_intent') {
1730
+ const body = {
1731
+ file_b64: fileToB64(args.file_path),
1732
+ intent: args.intent,
1733
+ };
1734
+ if (args.intent_params !== undefined) body.intent_params = args.intent_params;
1735
+ if (args.mode !== undefined) body.mode = args.mode;
1736
+ if (args.confirm !== undefined) body.confirm = args.confirm;
1737
+ const result = await callTool('xlsx_healer_intent', body);
1738
+ return applyFileB64(result, args.out_path);
1739
+ }
1740
+
1570
1741
  // All other tools (list_sheets, schema, hyperlinks, conditional_formats,
1571
1742
  // styles, etc.) — single-file relay. Forward any common option keys the
1572
1743
  // routes accept so we don't silently drop them. New keys added here as
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "xlsx-for-ai",
3
3
  "mcpName": "io.github.senoff/xlsx-for-ai",
4
- "version": "2.26.0",
4
+ "version": "3.0.0",
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": {
@@ -12,11 +12,12 @@
12
12
  "files": [
13
13
  "index.js",
14
14
  "mcp.js",
15
+ "lib/annotations.js",
15
16
  "lib/client.js",
16
17
  "lib/config.js",
17
- "lib/register.js",
18
- "lib/fallback-read.js",
19
18
  "lib/discover.js",
19
+ "lib/fallback-read.js",
20
+ "lib/register.js",
20
21
  "README.md",
21
22
  "SECURITY.md",
22
23
  "LICENSE"
@@ -25,6 +26,8 @@
25
26
  "test": "node --test test/v2/*.test.js",
26
27
  "build-manifests": "node scripts/build-manifests.js",
27
28
  "check-manifests": "node scripts/build-manifests.js --check",
29
+ "check-publish-allowlist": "node scripts/check-publish-allowlist.js",
30
+ "prepublishOnly": "node scripts/check-publish-allowlist.js && node scripts/build-manifests.js --check",
28
31
  "prepare": "husky"
29
32
  },
30
33
  "keywords": [