@imenam/xls-reader 2.1.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 +20 -3
- package/dist/excel.js +37 -16
- package/dist/index.js +33 -9
- package/package.json +1 -1
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` |
|
|
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,7 +37,9 @@ 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` |
|
|
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` |
|
|
26
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 |
|
|
27
44
|
| `format` | `"markdown" \| "json" \| "json-file"` | ❌ | Output format (default: `markdown`) |
|
|
28
45
|
| `output_path` | `string` | ⚠️ | Destination file for the JSON export (required when `format` is `json-file`) |
|
|
@@ -62,7 +79,7 @@ With `format: "json"` you get a structured object — better when the agent need
|
|
|
62
79
|
- With `header_row: false`, each row is an **array** of values (empty cells become `null`).
|
|
63
80
|
- When data is truncated, `truncated` is `true` and a `note` field explains what was omitted.
|
|
64
81
|
|
|
65
|
-
With `format: "json-file"` the data (no size limit) is written to `output_path` 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:
|
|
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:
|
|
66
83
|
|
|
67
84
|
```json
|
|
68
85
|
{
|
package/dist/excel.js
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import XLSX from "xlsx";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.");
|
|
7
9
|
}
|
|
8
|
-
const
|
|
9
|
-
|
|
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";
|
|
34
|
+
}
|
|
35
|
+
export function listSheets(source) {
|
|
36
|
+
return loadWorkbook(source).SheetNames;
|
|
10
37
|
}
|
|
11
38
|
// ─── Cell value helpers ─────────────────────────────────────────────────────
|
|
12
39
|
function isEmptyValue(value) {
|
|
@@ -140,11 +167,8 @@ function renderMarkdown(data) {
|
|
|
140
167
|
return result;
|
|
141
168
|
}
|
|
142
169
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
143
|
-
export function readSheet(
|
|
144
|
-
|
|
145
|
-
throw new Error(`File not found: ${filePath}`);
|
|
146
|
-
}
|
|
147
|
-
const workbook = XLSX.readFile(filePath, { cellDates: true });
|
|
170
|
+
export function readSheet(source, sheetName, options = {}) {
|
|
171
|
+
const workbook = loadWorkbook(source, { cellDates: true });
|
|
148
172
|
if (!workbook.SheetNames.includes(sheetName)) {
|
|
149
173
|
throw new Error(`Sheet '${sheetName}' not found. Available: ${workbook.SheetNames.join(", ")}`);
|
|
150
174
|
}
|
|
@@ -163,11 +187,8 @@ export function readSheet(filePath, sheetName, options = {}) {
|
|
|
163
187
|
* Read a workbook into a single JSON object, with no size limit.
|
|
164
188
|
* Exports every sheet, or only `sheetName` when provided.
|
|
165
189
|
*/
|
|
166
|
-
export function readWorkbook(
|
|
167
|
-
|
|
168
|
-
throw new Error(`File not found: ${filePath}`);
|
|
169
|
-
}
|
|
170
|
-
const workbook = XLSX.readFile(filePath, { cellDates: true });
|
|
190
|
+
export function readWorkbook(source, options = {}) {
|
|
191
|
+
const workbook = loadWorkbook(source, { cellDates: true });
|
|
171
192
|
let names = workbook.SheetNames;
|
|
172
193
|
if (options.sheetName) {
|
|
173
194
|
if (!names.includes(options.sheetName)) {
|
|
@@ -182,5 +203,5 @@ export function readWorkbook(filePath, options = {}) {
|
|
|
182
203
|
});
|
|
183
204
|
return { sheet: name, groups: data ? buildGroups(data) : [] };
|
|
184
205
|
});
|
|
185
|
-
return JSON.stringify({ file:
|
|
206
|
+
return JSON.stringify({ file: sourceLabel(source), sheets }, null, 2);
|
|
186
207
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,12 +16,34 @@ const server = new McpServer({
|
|
|
16
16
|
name: "xls-reader",
|
|
17
17
|
version: "1.0.0",
|
|
18
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
|
+
}
|
|
19
40
|
// ─── list_sheets ──────────────────────────────────────────────────────────────
|
|
20
|
-
server.tool("list_sheets",
|
|
21
|
-
|
|
22
|
-
|
|
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) => {
|
|
23
45
|
try {
|
|
24
|
-
const sheets = listSheets(
|
|
46
|
+
const sheets = listSheets(toSource(args));
|
|
25
47
|
return {
|
|
26
48
|
content: [{ type: "text", text: sheets.join("\n") }],
|
|
27
49
|
};
|
|
@@ -37,13 +59,14 @@ server.tool("list_sheets", "List all sheet names (tabs) in an Excel file (.xlsx,
|
|
|
37
59
|
// ─── get_sheet ────────────────────────────────────────────────────────────────
|
|
38
60
|
server.tool("get_sheet", [
|
|
39
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.",
|
|
40
63
|
"Empty rows are treated as section separators — they close the current table and start a new one.",
|
|
41
64
|
"format='markdown' (default) returns Markdown table(s); format='json' returns { sheet, truncated, groups }",
|
|
42
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.",
|
|
43
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.",
|
|
44
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.",
|
|
45
68
|
].join(" "), {
|
|
46
|
-
|
|
69
|
+
...sourceShape,
|
|
47
70
|
sheet_name: z
|
|
48
71
|
.string()
|
|
49
72
|
.optional()
|
|
@@ -55,7 +78,7 @@ server.tool("get_sheet", [
|
|
|
55
78
|
output_path: z
|
|
56
79
|
.string()
|
|
57
80
|
.optional()
|
|
58
|
-
.describe("Destination
|
|
81
|
+
.describe("Destination path on the server's disk for the JSON export (required when format is 'json-file')"),
|
|
59
82
|
cell_budget: z
|
|
60
83
|
.number()
|
|
61
84
|
.optional()
|
|
@@ -66,8 +89,9 @@ server.tool("get_sheet", [
|
|
|
66
89
|
.boolean()
|
|
67
90
|
.optional()
|
|
68
91
|
.describe("Treat the first row as a column header (default: true)"),
|
|
69
|
-
}, async ({
|
|
92
|
+
}, async ({ sheet_name, format, output_path, cell_budget, max_cols, max_rows, header_row, ...rest }) => {
|
|
70
93
|
try {
|
|
94
|
+
const source = toSource(rest);
|
|
71
95
|
if (format === "json-file") {
|
|
72
96
|
if (!output_path) {
|
|
73
97
|
return {
|
|
@@ -75,7 +99,7 @@ server.tool("get_sheet", [
|
|
|
75
99
|
isError: true,
|
|
76
100
|
};
|
|
77
101
|
}
|
|
78
|
-
const json = readWorkbook(
|
|
102
|
+
const json = readWorkbook(source, { headerRow: header_row, sheetName: sheet_name });
|
|
79
103
|
const resolved = path.resolve(output_path);
|
|
80
104
|
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
81
105
|
fs.writeFileSync(resolved, json, "utf-8");
|
|
@@ -89,7 +113,7 @@ server.tool("get_sheet", [
|
|
|
89
113
|
isError: true,
|
|
90
114
|
};
|
|
91
115
|
}
|
|
92
|
-
const output = readSheet(
|
|
116
|
+
const output = readSheet(source, sheet_name, {
|
|
93
117
|
format,
|
|
94
118
|
cellBudget: cell_budget,
|
|
95
119
|
maxCols: max_cols,
|
package/package.json
CHANGED