@imenam/xls-reader 2.0.0 → 2.2.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
@@ -4,6 +4,19 @@ An MCP (Model Context Protocol) server that lets AI agents read Excel files. It
4
4
 
5
5
  Supports `.xlsx`, `.xls`, `.ods` and `.csv` (anything [SheetJS](https://sheetjs.com/) can read).
6
6
 
7
+ ## Where the file comes from
8
+
9
+ Both tools take the workbook either **by path** or **by content**:
10
+
11
+ - `file_path` — a path on the machine *running the server*. This is what a caller provides.
12
+ - `file_content` — the file itself, base64-encoded (a `data:…;base64,` prefix is accepted), with `file_name` to label the output.
13
+
14
+ At least one of the two is required. When both are given, `file_content` wins.
15
+
16
+ **When the server runs on another machine, the content is what must travel** — a path would point at a file the server cannot see. That translation is not the agent's job: the client-side relay does it. With [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway) in `--mcp` mode, the relay runs next to the agent, reads the file named by `file_path`, sends `file_content`, and hides `file_content` / `file_name` from the schema it publishes — so the agent only ever names a path. Enable it with `GATEWAY_INLINE_ROOTS`, which also confines *which* folders can be read; see that package's README.
17
+
18
+ Over stdio on a single machine, `file_path` is read directly and nothing else is needed.
19
+
7
20
  ## Tools
8
21
 
9
22
  ### `list_sheets`
@@ -12,7 +25,9 @@ Lists all sheet names (tabs) in a workbook.
12
25
 
13
26
  | Parameter | Type | Required | Description |
14
27
  |---|---|---|---|
15
- | `file_path` | `string` | ✅ | Absolute or relative path to the Excel file |
28
+ | `file_path` | `string` | ⚠️ | Path to the Excel file. Resolved on the server, or read by the relay (see above) |
29
+ | `file_content` | `string` | ⚠️ | Base64 content of the file. Normally filled in by the relay, not by the agent |
30
+ | `file_name` | `string` | ❌ | Original file name, used only for labelling. Filled in with `file_content` |
16
31
 
17
32
  Returns the sheet names, one per line.
18
33
 
@@ -22,8 +37,12 @@ Reads a sheet and returns its content as a Markdown table.
22
37
 
23
38
  | Parameter | Type | Required | Description |
24
39
  |---|---|---|---|
25
- | `file_path` | `string` | ✅ | Absolute or relative path to the Excel file |
26
- | `sheet_name` | `string` | ✅ | Name of the sheet to read |
40
+ | `file_path` | `string` | ⚠️ | Path to the Excel file. Resolved on the server, or read by the relay (see above) |
41
+ | `file_content` | `string` | ⚠️ | Base64 content of the file. Normally filled in by the relay, not by the agent |
42
+ | `file_name` | `string` | ❌ | Original file name, used only for labelling. Filled in with `file_content` |
43
+ | `sheet_name` | `string` | ⚠️ | Name of the sheet to read. Required for `markdown`/`json`. For `json-file`: omit to export the whole workbook, or set it to export only that sheet |
44
+ | `format` | `"markdown" \| "json" \| "json-file"` | ❌ | Output format (default: `markdown`) |
45
+ | `output_path` | `string` | ⚠️ | Destination file for the JSON export (required when `format` is `json-file`) |
27
46
  | `cell_budget` | `number` | ❌ | Maximum number of cells to return (cols × rows). Default: `2000` |
28
47
  | `max_cols` | `number` | ❌ | Override the maximum number of columns |
29
48
  | `max_rows` | `number` | ❌ | Override the maximum number of rows |
@@ -36,6 +55,42 @@ Reads a sheet and returns its content as a Markdown table.
36
55
  - **In-cell line breaks** are converted to a literal `\n` so they don't break the Markdown table row.
37
56
  - **Dates and booleans** are rendered as readable text rather than Excel serial numbers.
38
57
 
58
+ #### Output formats
59
+
60
+ With `format: "markdown"` (default) you get one Markdown table per section.
61
+
62
+ With `format: "json"` you get a structured object — better when the agent needs to *process* the data rather than read it. Native types are preserved (numbers, booleans, dates as ISO strings) and real line breaks are kept:
63
+
64
+ ```json
65
+ {
66
+ "sheet": "Feuille1",
67
+ "truncated": false,
68
+ "groups": [
69
+ [
70
+ { "Nom": "Dupont", "Prénom": "Jean", "Age": 42 },
71
+ { "Nom": "Martin", "Age": 35 }
72
+ ]
73
+ ]
74
+ }
75
+ ```
76
+
77
+ - `groups` is an array of sections (one per block separated by an empty row).
78
+ - With `header_row: true`, each row is an **object** keyed by column name; **empty cells are omitted**.
79
+ - With `header_row: false`, each row is an **array** of values (empty cells become `null`).
80
+ - When data is truncated, `truncated` is `true` and a `note` field explains what was omitted.
81
+
82
+ With `format: "json-file"` the data (no size limit) is written to `output_path` **on the server's disk** and the tool returns the saved path. Use this when the data is too large to return inline. By default the **entire workbook** is exported; provide `sheet_name` to export only that one sheet. Size limits are ignored. The file content is:
83
+
84
+ ```json
85
+ {
86
+ "file": "data.xlsx",
87
+ "sheets": [
88
+ { "sheet": "Feuille1", "groups": [ /* …same shape as json… */ ] },
89
+ { "sheet": "Feuille2", "groups": [ /* … */ ] }
90
+ ]
91
+ }
92
+ ```
93
+
39
94
  #### Size limit
40
95
 
41
96
  By default the response is capped at `cell_budget = 2000` cells (`cols × rows`). With 10 columns you get ~200 rows; with 20 columns, ~100 rows. When data is truncated, a warning is appended telling you how much was omitted. Override with `cell_budget`, `max_cols`, or `max_rows` to read more (or less).
package/dist/excel.js CHANGED
@@ -1,32 +1,73 @@
1
1
  import XLSX from "xlsx";
2
2
  import fs from "fs";
3
- export function listSheets(filePath) {
4
- if (!fs.existsSync(filePath)) {
5
- throw new Error(`File not found: ${filePath}`);
3
+ import path from "path";
4
+ const BASE64_DATA_URL = /^data:[^;,]*(;[^;,]+)*;base64,/i;
5
+ function decodeBase64(content) {
6
+ const cleaned = content.replace(BASE64_DATA_URL, "").replace(/\s+/g, "");
7
+ if (cleaned === "") {
8
+ throw new Error("file_content is empty.");
6
9
  }
7
- const workbook = XLSX.readFile(filePath);
8
- return workbook.SheetNames;
10
+ const buffer = Buffer.from(cleaned, "base64");
11
+ if (buffer.length === 0) {
12
+ throw new Error("file_content is not valid base64 data.");
13
+ }
14
+ return buffer;
15
+ }
16
+ function loadWorkbook(source, options = {}) {
17
+ if (source.fileContent != null && source.fileContent !== "") {
18
+ return XLSX.read(decodeBase64(source.fileContent), { ...options, type: "buffer" });
19
+ }
20
+ if (!source.filePath) {
21
+ throw new Error("Either file_path or file_content must be provided.");
22
+ }
23
+ if (!fs.existsSync(source.filePath)) {
24
+ throw new Error(`File not found: ${source.filePath}. If the file lives on another machine, send it with file_content (base64) instead.`);
25
+ }
26
+ return XLSX.readFile(source.filePath, options);
27
+ }
28
+ function sourceLabel(source) {
29
+ if (source.fileName)
30
+ return path.basename(source.fileName);
31
+ if (source.filePath)
32
+ return path.basename(source.filePath);
33
+ return "workbook";
9
34
  }
35
+ export function listSheets(source) {
36
+ return loadWorkbook(source).SheetNames;
37
+ }
38
+ // ─── Cell value helpers ─────────────────────────────────────────────────────
39
+ function isEmptyValue(value) {
40
+ return value === null || value === undefined || value === "";
41
+ }
42
+ /** Stringify a cell for Markdown (in-cell line breaks escaped to keep the table intact). */
10
43
  function cellToString(value) {
11
- if (value === null || value === undefined || value === "")
44
+ if (isEmptyValue(value))
12
45
  return "";
13
46
  if (value instanceof Date)
14
47
  return value.toLocaleDateString();
15
48
  if (typeof value === "boolean")
16
49
  return value ? "TRUE" : "FALSE";
17
- // Replace in-cell line breaks with a literal \n so they don't break the Markdown table row.
18
50
  return String(value).trim().replace(/\r\n|\r|\n/g, "\\n");
19
51
  }
20
- function toStringRow(raw, numCols) {
52
+ /** Convert a cell to a native JSON value (types preserved, dates as ISO, real line breaks kept). */
53
+ function cellToJson(value) {
54
+ if (value instanceof Date)
55
+ return value.toISOString();
56
+ if (typeof value === "string")
57
+ return value.trim();
58
+ return value;
59
+ }
60
+ function toStringCells(raw, numCols) {
21
61
  return Array.from({ length: numCols }, (_, i) => cellToString(raw[i]));
22
62
  }
23
- function isEmptyRow(cells) {
24
- return cells.every((c) => c === "");
63
+ // ─── Sections (an empty row delimits a new table) ───────────────────────────
64
+ function isEmptyRawRow(row) {
65
+ return row.every(isEmptyValue);
25
66
  }
26
67
  function groupIntoSections(rows) {
27
68
  const sections = [[]];
28
69
  for (const row of rows) {
29
- if (isEmptyRow(row)) {
70
+ if (isEmptyRawRow(row)) {
30
71
  if (sections[sections.length - 1].length > 0) {
31
72
  sections.push([]);
32
73
  }
@@ -37,76 +78,130 @@ function groupIntoSections(rows) {
37
78
  }
38
79
  return sections.filter((s) => s.length > 0);
39
80
  }
81
+ /** Read a worksheet into header + grouped sections, applying the size limit unless `unlimited`. */
82
+ function extractSheetData(ws, options) {
83
+ const rawRows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: "" });
84
+ if (rawRows.length === 0)
85
+ return null;
86
+ const { cellBudget = 2000, maxCols, maxRows, headerRow = true, unlimited = false } = options;
87
+ const totalCols = rawRows.reduce((max, row) => Math.max(max, row.length), 0);
88
+ let effectiveCols = totalCols;
89
+ let effectiveRows = rawRows.length;
90
+ let note = null;
91
+ if (!unlimited) {
92
+ effectiveCols = Math.min(totalCols, maxCols ?? totalCols);
93
+ effectiveRows = Math.min(rawRows.length, maxRows ?? Math.floor(cellBudget / Math.max(effectiveCols, 1)));
94
+ const truncatedCols = effectiveCols < totalCols;
95
+ const truncatedRows = effectiveRows < rawRows.length;
96
+ if (truncatedCols || truncatedRows) {
97
+ const w = [];
98
+ if (truncatedCols)
99
+ w.push(`${effectiveCols} columns out of ${totalCols}`);
100
+ if (truncatedRows)
101
+ w.push(`${effectiveRows} rows out of ${rawRows.length}`);
102
+ note = `Display limited to ${w.join(", ")}. Use max_cols / max_rows / cell_budget to adjust.`;
103
+ }
104
+ }
105
+ const matrix = rawRows
106
+ .slice(0, effectiveRows)
107
+ .map((row) => row.slice(0, effectiveCols));
108
+ let headerRaw = null;
109
+ let dataRows = matrix;
110
+ if (headerRow && matrix.length > 0) {
111
+ headerRaw = matrix[0];
112
+ dataRows = matrix.slice(1);
113
+ }
114
+ return { headerRaw, sections: groupIntoSections(dataRows), numCols: effectiveCols, note };
115
+ }
116
+ // ─── JSON groups builder (shared by single-sheet and whole-workbook output) ──
117
+ function buildHeaderKeys(headerRaw, numCols) {
118
+ return Array.from({ length: numCols }, (_, i) => {
119
+ const key = cellToString(headerRaw[i]);
120
+ return key === "" ? `column_${i + 1}` : key;
121
+ });
122
+ }
123
+ function rowToObject(row, keys) {
124
+ const obj = {};
125
+ keys.forEach((key, i) => {
126
+ if (!isEmptyValue(row[i]))
127
+ obj[key] = cellToJson(row[i]);
128
+ });
129
+ return obj;
130
+ }
131
+ function rowToArray(row, numCols) {
132
+ return Array.from({ length: numCols }, (_, i) => isEmptyValue(row[i]) ? null : cellToJson(row[i]));
133
+ }
134
+ /** Turn a sheet's sections into JSON groups (objects keyed by header, or arrays of values). */
135
+ function buildGroups(data) {
136
+ if (data.headerRaw) {
137
+ const keys = buildHeaderKeys(data.headerRaw, data.numCols);
138
+ return data.sections.map((section) => section.map((row) => rowToObject(row, keys)));
139
+ }
140
+ return data.sections.map((section) => section.map((row) => rowToArray(row, data.numCols)));
141
+ }
142
+ // ─── Markdown renderer ──────────────────────────────────────────────────────
40
143
  function formatRow(cells, numCols) {
41
144
  const padded = Array.from({ length: numCols }, (_, i) => cells[i] ?? "");
42
145
  return "| " + padded.join(" | ") + " |";
43
146
  }
44
- function sectionToMarkdown(section, header, numCols) {
45
- const lines = [];
46
- if (header) {
47
- lines.push(formatRow(header, numCols));
48
- const sep = Array.from({ length: numCols }, (_, i) => "-".repeat(Math.max((header[i] ?? "").length, 3)));
49
- lines.push("| " + sep.join(" | ") + " |");
50
- }
51
- for (const row of section) {
52
- lines.push(formatRow(row, numCols));
53
- }
54
- return lines.join("\n");
147
+ function markdownHeader(header, numCols) {
148
+ const sep = Array.from({ length: numCols }, (_, i) => "-".repeat(Math.max((header[i] ?? "").length, 3)));
149
+ return [formatRow(header, numCols), "| " + sep.join(" | ") + " |"];
55
150
  }
56
- export function readSheet(filePath, sheetName, options = {}) {
57
- if (!fs.existsSync(filePath)) {
58
- throw new Error(`File not found: ${filePath}`);
151
+ function renderMarkdown(data) {
152
+ const header = data.headerRaw ? toStringCells(data.headerRaw, data.numCols) : null;
153
+ if (data.sections.length === 0) {
154
+ if (!header)
155
+ return "_Empty sheet._";
156
+ return [...markdownHeader(header, data.numCols), "_No data rows._"].join("\n");
59
157
  }
60
- const workbook = XLSX.readFile(filePath, { cellDates: true });
158
+ const parts = data.sections.map((section) => {
159
+ const lines = header ? markdownHeader(header, data.numCols) : [];
160
+ for (const row of section)
161
+ lines.push(formatRow(toStringCells(row, data.numCols), data.numCols));
162
+ return lines.join("\n");
163
+ });
164
+ let result = parts.join("\n\n");
165
+ if (data.note)
166
+ result += `\n\n> ⚠ ${data.note}`;
167
+ return result;
168
+ }
169
+ // ─── Public API ─────────────────────────────────────────────────────────────
170
+ export function readSheet(source, sheetName, options = {}) {
171
+ const workbook = loadWorkbook(source, { cellDates: true });
61
172
  if (!workbook.SheetNames.includes(sheetName)) {
62
173
  throw new Error(`Sheet '${sheetName}' not found. Available: ${workbook.SheetNames.join(", ")}`);
63
174
  }
64
- const ws = workbook.Sheets[sheetName];
65
- const rawRows = XLSX.utils.sheet_to_json(ws, {
66
- header: 1,
67
- defval: "",
68
- });
69
- if (rawRows.length === 0) {
70
- return "_Empty sheet._";
71
- }
72
- const { cellBudget = 2000, maxCols, maxRows, headerRow = true } = options;
73
- const totalCols = rawRows.reduce((max, row) => Math.max(max, row.length), 0);
74
- const effectiveCols = Math.min(totalCols, maxCols ?? totalCols);
75
- const effectiveRows = Math.min(rawRows.length, maxRows ?? Math.floor(cellBudget / Math.max(effectiveCols, 1)));
76
- const truncatedCols = effectiveCols < totalCols;
77
- const truncatedRows = effectiveRows < rawRows.length;
78
- const stringRows = rawRows
79
- .slice(0, effectiveRows)
80
- .map((row) => toStringRow(row, effectiveCols));
81
- let header = null;
82
- let dataRows = stringRows;
83
- if (headerRow && stringRows.length > 0) {
84
- header = stringRows[0];
85
- dataRows = stringRows.slice(1);
175
+ const { format = "markdown", ...rest } = options;
176
+ const data = extractSheetData(workbook.Sheets[sheetName], rest);
177
+ if (format === "json") {
178
+ const out = { sheet: sheetName, truncated: data?.note != null };
179
+ if (data?.note)
180
+ out.note = data.note;
181
+ out.groups = data ? buildGroups(data) : [];
182
+ return JSON.stringify(out, null, 2);
86
183
  }
87
- const sections = groupIntoSections(dataRows);
88
- if (sections.length === 0) {
89
- const lines = [];
90
- if (header) {
91
- lines.push(formatRow(header, effectiveCols));
92
- const sep = Array.from({ length: effectiveCols }, (_, i) => "-".repeat(Math.max((header[i] ?? "").length, 3)));
93
- lines.push("| " + sep.join(" | ") + " |");
94
- lines.push("_No data rows._");
95
- }
96
- else {
97
- return "_Empty sheet._";
184
+ return data ? renderMarkdown(data) : "_Empty sheet._";
185
+ }
186
+ /**
187
+ * Read a workbook into a single JSON object, with no size limit.
188
+ * Exports every sheet, or only `sheetName` when provided.
189
+ */
190
+ export function readWorkbook(source, options = {}) {
191
+ const workbook = loadWorkbook(source, { cellDates: true });
192
+ let names = workbook.SheetNames;
193
+ if (options.sheetName) {
194
+ if (!names.includes(options.sheetName)) {
195
+ throw new Error(`Sheet '${options.sheetName}' not found. Available: ${names.join(", ")}`);
98
196
  }
99
- return lines.join("\n");
100
- }
101
- const parts = sections.map((section) => sectionToMarkdown(section, header, effectiveCols));
102
- let result = parts.join("\n\n");
103
- if (truncatedCols || truncatedRows) {
104
- const warnings = [];
105
- if (truncatedCols)
106
- warnings.push(`${effectiveCols} columns out of ${totalCols}`);
107
- if (truncatedRows)
108
- warnings.push(`${effectiveRows} rows out of ${rawRows.length}`);
109
- result += `\n\n> ⚠ Display limited to ${warnings.join(", ")}. Use \`max_cols\` / \`max_rows\` / \`cell_budget\` to adjust.`;
197
+ names = [options.sheetName];
110
198
  }
111
- return result;
199
+ const sheets = names.map((name) => {
200
+ const data = extractSheetData(workbook.Sheets[name], {
201
+ headerRow: options.headerRow,
202
+ unlimited: true,
203
+ });
204
+ return { sheet: name, groups: data ? buildGroups(data) : [] };
205
+ });
206
+ return JSON.stringify({ file: sourceLabel(source), sheets }, null, 2);
112
207
  }
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import dotenv from "dotenv";
5
+ import fs from "fs";
5
6
  import path from "path";
6
7
  import { fileURLToPath } from "url";
7
8
  import { z } from "zod";
8
9
  import { setupLogging } from "./logger.js";
9
- import { listSheets, readSheet } from "./excel.js";
10
+ import { listSheets, readSheet, readWorkbook } from "./excel.js";
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  const rootDir = path.resolve(__dirname, "..");
12
13
  dotenv.config({ path: path.join(rootDir, ".env"), override: true });
@@ -15,12 +16,34 @@ const server = new McpServer({
15
16
  name: "xls-reader",
16
17
  version: "1.0.0",
17
18
  });
19
+ // ─── Workbook source (path on this server, or inline content) ─────────────────
20
+ const sourceShape = {
21
+ file_path: z
22
+ .string()
23
+ .optional()
24
+ .describe("Path to the Excel file on the machine running this server. Use it only when the file is local to the server; otherwise send file_content"),
25
+ file_content: z
26
+ .string()
27
+ .optional()
28
+ .describe("Base64-encoded content of the Excel file (a 'data:' URL prefix is accepted). Filled in by the client-side relay, which reads the file named by file_path on the caller's machine — a caller that provides file_path has nothing to put here. Takes precedence over file_path"),
29
+ file_name: z
30
+ .string()
31
+ .optional()
32
+ .describe("Original file name, used only to label the output. Also filled in by the relay alongside file_content"),
33
+ };
34
+ function toSource(args) {
35
+ if (!args.file_path && !args.file_content) {
36
+ throw new Error("Provide either file_path (file local to the server) or file_content (base64 content of the file).");
37
+ }
38
+ return { filePath: args.file_path, fileContent: args.file_content, fileName: args.file_name };
39
+ }
18
40
  // ─── list_sheets ──────────────────────────────────────────────────────────────
19
- server.tool("list_sheets", "List all sheet names (tabs) in an Excel file (.xlsx, .xls, .ods, .csv).", {
20
- file_path: z.string().describe("Absolute or relative path to the Excel file"),
21
- }, async ({ file_path }) => {
41
+ server.tool("list_sheets", [
42
+ "List all sheet names (tabs) in an Excel file (.xlsx, .xls, .ods, .csv).",
43
+ "Pass file_path when the file sits on the machine running this server, or file_content (base64) when it does not.",
44
+ ].join(" "), sourceShape, async (args) => {
22
45
  try {
23
- const sheets = listSheets(file_path);
46
+ const sheets = listSheets(toSource(args));
24
47
  return {
25
48
  content: [{ type: "text", text: sheets.join("\n") }],
26
49
  };
@@ -35,12 +58,27 @@ server.tool("list_sheets", "List all sheet names (tabs) in an Excel file (.xlsx,
35
58
  });
36
59
  // ─── get_sheet ────────────────────────────────────────────────────────────────
37
60
  server.tool("get_sheet", [
38
- "Read an Excel sheet and return its content as a Markdown table.",
61
+ "Read an Excel sheet and return its content.",
62
+ "Pass file_path when the file sits on the machine running this server, or file_content (base64) when it does not.",
39
63
  "Empty rows are treated as section separators — they close the current table and start a new one.",
40
- "Data is limited by a cell budget (cols × rows, default 2000). Use max_cols / max_rows / cell_budget to override.",
64
+ "format='markdown' (default) returns Markdown table(s); format='json' returns { sheet, truncated, groups }",
65
+ "where groups is an array of sections. With header_row, each row is an object keyed by column name (empty cells omitted, types preserved); otherwise each row is an array of values.",
66
+ "format='json-file' writes JSON (no size limit) to output_path (required) and returns the path; it exports the whole workbook, or only sheet_name when provided. Size limits are ignored.",
67
+ "Data is limited by a cell budget (cols × rows, default 2000) for 'markdown' and 'json'. Use max_cols / max_rows / cell_budget to override.",
41
68
  ].join(" "), {
42
- file_path: z.string().describe("Absolute or relative path to the Excel file"),
43
- sheet_name: z.string().describe("Name of the sheet to read"),
69
+ ...sourceShape,
70
+ sheet_name: z
71
+ .string()
72
+ .optional()
73
+ .describe("Name of the sheet to read. Required for 'markdown'/'json'. For 'json-file': omit to export the whole workbook, or provide it to export only that sheet"),
74
+ format: z
75
+ .enum(["markdown", "json", "json-file"])
76
+ .optional()
77
+ .describe("Output format: 'markdown' (default), 'json', or 'json-file' (whole workbook to disk)"),
78
+ output_path: z
79
+ .string()
80
+ .optional()
81
+ .describe("Destination path on the server's disk for the JSON export (required when format is 'json-file')"),
44
82
  cell_budget: z
45
83
  .number()
46
84
  .optional()
@@ -51,16 +89,39 @@ server.tool("get_sheet", [
51
89
  .boolean()
52
90
  .optional()
53
91
  .describe("Treat the first row as a column header (default: true)"),
54
- }, async ({ file_path, sheet_name, cell_budget, max_cols, max_rows, header_row }) => {
92
+ }, async ({ sheet_name, format, output_path, cell_budget, max_cols, max_rows, header_row, ...rest }) => {
55
93
  try {
56
- const markdown = readSheet(file_path, sheet_name, {
94
+ const source = toSource(rest);
95
+ if (format === "json-file") {
96
+ if (!output_path) {
97
+ return {
98
+ content: [{ type: "text", text: "Error: output_path is required when format is 'json-file'." }],
99
+ isError: true,
100
+ };
101
+ }
102
+ const json = readWorkbook(source, { headerRow: header_row, sheetName: sheet_name });
103
+ const resolved = path.resolve(output_path);
104
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
105
+ fs.writeFileSync(resolved, json, "utf-8");
106
+ return {
107
+ content: [{ type: "text", text: `Workbook exported to ${resolved}` }],
108
+ };
109
+ }
110
+ if (!sheet_name) {
111
+ return {
112
+ content: [{ type: "text", text: "Error: sheet_name is required." }],
113
+ isError: true,
114
+ };
115
+ }
116
+ const output = readSheet(source, sheet_name, {
117
+ format,
57
118
  cellBudget: cell_budget,
58
119
  maxCols: max_cols,
59
120
  maxRows: max_rows,
60
121
  headerRow: header_row,
61
122
  });
62
123
  return {
63
- content: [{ type: "text", text: markdown }],
124
+ content: [{ type: "text", text: output }],
64
125
  };
65
126
  }
66
127
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/xls-reader",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server that lets AI agents read Excel files: list sheets and render any sheet as a Markdown table.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",