@imenam/xls-reader 2.2.0 → 2.4.1

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,145 +1,164 @@
1
- # @imenam/xls-reader
2
-
3
- An MCP (Model Context Protocol) server that lets AI agents read Excel files. It can list the sheets of a workbook and render any sheet as a Markdown table, with smart handling of empty rows, empty cells, and in-cell line breaks, plus a configurable size limit to keep responses LLM-friendly.
4
-
5
- Supports `.xlsx`, `.xls`, `.ods` and `.csv` (anything [SheetJS](https://sheetjs.com/) can read).
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
-
20
- ## Tools
21
-
22
- ### `list_sheets`
23
-
24
- Lists all sheet names (tabs) in a workbook.
25
-
26
- | Parameter | Type | Required | Description |
27
- |---|---|---|---|
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` |
31
-
32
- Returns the sheet names, one per line.
33
-
34
- ### `get_sheet`
35
-
36
- Reads a sheet and returns its content as a Markdown table.
37
-
38
- | Parameter | Type | Required | Description |
39
- |---|---|---|---|
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`) |
46
- | `cell_budget` | `number` | ❌ | Maximum number of cells to return (cols × rows). Default: `2000` |
47
- | `max_cols` | `number` | ❌ | Override the maximum number of columns |
48
- | `max_rows` | `number` | ❌ | Override the maximum number of rows |
49
- | `header_row` | `boolean` | ❌ | Treat the first row as a column header (default: `true`) |
50
-
51
- #### Smart content handling
52
-
53
- - **Empty rows are section separators.** A fully empty row closes the current table and starts a new one; when `header_row` is enabled, the header is repeated on each section.
54
- - **Empty cells are kept as blanks** so columns stay aligned.
55
- - **In-cell line breaks** are converted to a literal `\n` so they don't break the Markdown table row.
56
- - **Dates and booleans** are rendered as readable text rather than Excel serial numbers.
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
-
94
- #### Size limit
95
-
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).
97
-
98
- ## Installation
99
-
100
- ```bash
101
- npm install -g @imenam/xls-reader
102
- ```
103
-
104
- Or use it directly with `npx`:
105
-
106
- ```json
107
- {
108
- "xls-reader": {
109
- "command": "npx",
110
- "args": ["-y", "@imenam/xls-reader"]
111
- }
112
- }
113
- ```
114
-
115
- Or point to a local build:
116
-
117
- ```json
118
- {
119
- "xls-reader": {
120
- "command": "node",
121
- "args": ["/absolute/path/to/xls-reader/dist/index.js"]
122
- }
123
- }
124
- ```
125
-
126
- ## Configuration
127
-
128
- No environment variables are required. Optionally, set `MCP_LOG_DIR` to change where logs are written (default: `C:\var\log\xls-reader` on Windows, `/var/log/xls-reader` otherwise).
129
-
130
- ```env
131
- # .env
132
- MCP_LOG_DIR=/absolute/path/to/logs
133
- ```
134
-
135
- ## Development
136
-
137
- ```bash
138
- npm install
139
- npm run build # compile TypeScript to dist/
140
- npm run dev # run from source with tsx
141
- ```
142
-
143
- ## License
144
-
145
- MIT
1
+ # @imenam/xls-reader
2
+
3
+ An MCP (Model Context Protocol) server that lets AI agents read Excel files. It can list the sheets of a workbook and render any sheet as a Markdown table, with smart handling of empty rows, empty cells, and in-cell line breaks, plus a configurable size limit to keep responses LLM-friendly.
4
+
5
+ Supports `.xlsx`, `.xls`, `.ods` and `.csv` (anything [SheetJS](https://sheetjs.com/) can read).
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
+
20
+ ## Tools
21
+
22
+ ### `list_sheets`
23
+
24
+ Lists all sheet names (tabs) in a workbook.
25
+
26
+ | Parameter | Type | Required | Description |
27
+ |---|---|---|---|
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` |
31
+
32
+ Returns the sheet names, one per line.
33
+
34
+ ### `get_sheet`
35
+
36
+ Reads a sheet and returns its content as a Markdown table.
37
+
38
+ | Parameter | Type | Required | Description |
39
+ |---|---|---|---|
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 |
44
+ | `format` | `"markdown" \| "json"` | ❌ | Output format (default: `markdown`) |
45
+ | `cell_budget` | `number` | ❌ | Maximum number of cells to return (cols × rows). Default: `2000` |
46
+ | `max_cols` | `number` | ❌ | Override the maximum number of columns |
47
+ | `max_rows` | `number` | ❌ | Override the maximum number of rows |
48
+ | `header_row` | `boolean` | ❌ | Treat the first row as a column header (default: `true`) |
49
+
50
+ #### Smart content handling
51
+
52
+ - **Empty rows are section separators.** A fully empty row closes the current table and starts a new one; when `header_row` is enabled, the header is repeated on each section.
53
+ - **Empty cells are kept as blanks** so columns stay aligned.
54
+ - **In-cell line breaks** are converted to a literal `\n` so they don't break the Markdown table row.
55
+ - **Dates and booleans** are rendered as readable text rather than Excel serial numbers.
56
+
57
+ #### Output formats
58
+
59
+ With `format: "markdown"` (default) you get one Markdown table per section.
60
+
61
+ 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:
62
+
63
+ ```json
64
+ {
65
+ "sheet": "Feuille1",
66
+ "truncated": false,
67
+ "groups": [
68
+ [
69
+ { "Nom": "Dupont", "Prénom": "Jean", "Age": 42 },
70
+ { "Nom": "Martin", "Age": 35 }
71
+ ]
72
+ ]
73
+ }
74
+ ```
75
+
76
+ - `groups` is an array of sections (one per block separated by an empty row).
77
+ - With `header_row: true`, each row is an **object** keyed by column name; **empty cells are omitted**.
78
+ - With `header_row: false`, each row is an **array** of values (empty cells become `null`).
79
+ - When data is truncated, `truncated` is `true` and a `note` field explains what was omitted.
80
+
81
+ To export without any size limit, use [`export_workbook`](#export_workbook), which writes a JSON file instead of returning it.
82
+
83
+ #### Size limit
84
+
85
+ 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).
86
+
87
+ ### `export_workbook`
88
+
89
+ Exports a workbook as a JSON file, with **no size limit**, and writes it **on the agent machine**. Use it when the data is too large to return inline. By default the entire workbook is exported; provide `sheet_name` to export only one sheet.
90
+
91
+ | Parameter | Type | Required | Description |
92
+ |---|---|---|---|
93
+ | `file_path` | `string` | ⚠️ | Path to the Excel file. Resolved on the server, or read by the relay (see above) |
94
+ | `file_content` | `string` | ⚠️ | Base64 content of the file. Normally filled in by the relay, not by the agent |
95
+ | `file_name` | `string` | ❌ | Original file name, used only for labelling. Filled in with `file_content` |
96
+ | `sheet_name` | `string` | ❌ | Omit to export the whole workbook, or set it to export only that sheet |
97
+ | `header_row` | `boolean` | ❌ | Treat the first row as a column header (default: `true`) |
98
+ | `destination_path` | `string` | ✅ | **Absolute directory** on the agent machine. Created if missing |
99
+ | `filename` | `string` | ✅ | Name the exported file gets there, e.g. `data.json`. No path separators |
100
+ | `destination_overwrite` | `boolean` | ❌ | Replace the destination file if it exists (default: `false`) |
101
+ | `destination_relay` | `boolean` | ❌ | Filled in by the relay, never by the agent |
102
+
103
+ The exported file never passes through the conversation. When this MCP runs next to the agent (stdio), it writes the file itself. Behind [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway) it runs on the gateway's machine and cannot reach the agent's disk: it then returns the bytes, and the relay — which *does* run on the agent machine — writes them at `destination_path`. The relay keeps that path to itself and never forwards it, which is why the tool schema declares it optional; the tool rejects a call that lacks it when it has to write the file itself. That path requires `GATEWAY_INLINE_WRITE_ROOTS` in the relay's `env` block; without it the call fails explicitly rather than dropping the file on the wrong machine.
104
+
105
+ The file content is:
106
+
107
+ ```json
108
+ {
109
+ "file": "data.xlsx",
110
+ "sheets": [
111
+ { "sheet": "Feuille1", "groups": [ /* …same shape as json… */ ] },
112
+ { "sheet": "Feuille2", "groups": [ /* … */ ] }
113
+ ]
114
+ }
115
+ ```
116
+
117
+ ## Installation
118
+
119
+ ```bash
120
+ npm install -g @imenam/xls-reader
121
+ ```
122
+
123
+ Or use it directly with `npx`:
124
+
125
+ ```json
126
+ {
127
+ "xls-reader": {
128
+ "command": "npx",
129
+ "args": ["-y", "@imenam/xls-reader"]
130
+ }
131
+ }
132
+ ```
133
+
134
+ Or point to a local build:
135
+
136
+ ```json
137
+ {
138
+ "xls-reader": {
139
+ "command": "node",
140
+ "args": ["/absolute/path/to/xls-reader/dist/index.js"]
141
+ }
142
+ }
143
+ ```
144
+
145
+ ## Configuration
146
+
147
+ No environment variables are required. Optionally, set `MCP_LOG_DIR` to change where logs are written (default: `C:\var\log\xls-reader` on Windows, `/var/log/xls-reader` otherwise).
148
+
149
+ ```env
150
+ # .env
151
+ MCP_LOG_DIR=/absolute/path/to/logs
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ npm install
158
+ npm run build # compile TypeScript to dist/
159
+ npm run dev # run from source with tsx
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT
package/dist/index.js CHANGED
@@ -2,12 +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";
6
5
  import path from "path";
7
6
  import { fileURLToPath } from "url";
8
7
  import { z } from "zod";
9
8
  import { setupLogging } from "./logger.js";
10
9
  import { listSheets, readSheet, readWorkbook } from "./excel.js";
10
+ import { deliverFile, RELAY_SHAPE_DESCRIPTIONS } from "./relay.js";
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
  const rootDir = path.resolve(__dirname, "..");
13
13
  dotenv.config({ path: path.join(rootDir, ".env"), override: true });
@@ -63,22 +63,17 @@ server.tool("get_sheet", [
63
63
  "Empty rows are treated as section separators — they close the current table and start a new one.",
64
64
  "format='markdown' (default) returns Markdown table(s); format='json' returns { sheet, truncated, groups }",
65
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.",
66
+ "To export without any size limit, use export_workbook, which writes a JSON file instead of returning it.",
67
+ "Data is limited by a cell budget (cols × rows, default 2000). Use max_cols / max_rows / cell_budget to override.",
68
68
  ].join(" "), {
69
69
  ...sourceShape,
70
70
  sheet_name: z
71
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"),
72
+ .describe("Name of the sheet to read"),
74
73
  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()
74
+ .enum(["markdown", "json"])
80
75
  .optional()
81
- .describe("Destination path on the server's disk for the JSON export (required when format is 'json-file')"),
76
+ .describe("Output format: 'markdown' (default) or 'json'"),
82
77
  cell_budget: z
83
78
  .number()
84
79
  .optional()
@@ -89,30 +84,9 @@ server.tool("get_sheet", [
89
84
  .boolean()
90
85
  .optional()
91
86
  .describe("Treat the first row as a column header (default: true)"),
92
- }, async ({ sheet_name, format, output_path, cell_budget, max_cols, max_rows, header_row, ...rest }) => {
87
+ }, async ({ sheet_name, format, cell_budget, max_cols, max_rows, header_row, ...rest }) => {
93
88
  try {
94
89
  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
90
  const output = readSheet(source, sheet_name, {
117
91
  format,
118
92
  cellBudget: cell_budget,
@@ -132,6 +106,59 @@ server.tool("get_sheet", [
132
106
  };
133
107
  }
134
108
  });
109
+ // ─── export_workbook ──────────────────────────────────────────────────────────
110
+ // L'export est un outil à part, et non un `format` de get_sheet : le relais rend
111
+ // obligatoire tout `<x>_path` dont il voit la paire, donc porter `destination_path` dans
112
+ // get_sheet forcerait une destination sur chaque simple lecture de feuille.
113
+ server.tool("export_workbook", [
114
+ "Export an Excel workbook as a JSON file written on the agent machine, with no size limit.",
115
+ "Exports the whole workbook, or only sheet_name when provided.",
116
+ "Pass file_path when the file sits on the machine running this server, or file_content (base64) when it does not.",
117
+ "The exported file never passes through the conversation.",
118
+ ].join(" "), {
119
+ ...sourceShape,
120
+ sheet_name: z
121
+ .string()
122
+ .optional()
123
+ .describe("Omit to export the whole workbook, or provide it to export only that sheet"),
124
+ header_row: z
125
+ .boolean()
126
+ .optional()
127
+ .describe("Treat the first row as a column header (default: true)"),
128
+ // Optionnel au niveau du schéma : le relais retient ce chemin et ne le transmet pas.
129
+ // deliverFile l'exige quand l'écriture a lieu ici.
130
+ destination_path: z
131
+ .string()
132
+ .optional()
133
+ .describe(`${RELAY_SHAPE_DESCRIPTIONS.destination_path}. Required`),
134
+ filename: z.string().describe(RELAY_SHAPE_DESCRIPTIONS.filename),
135
+ destination_overwrite: z
136
+ .boolean()
137
+ .optional()
138
+ .describe(RELAY_SHAPE_DESCRIPTIONS.destination_overwrite),
139
+ destination_relay: z
140
+ .boolean()
141
+ .optional()
142
+ .describe(RELAY_SHAPE_DESCRIPTIONS.destination_relay),
143
+ }, async ({ sheet_name, header_row, destination_path, filename, destination_overwrite, destination_relay, ...rest }) => {
144
+ try {
145
+ const json = readWorkbook(toSource(rest), { headerRow: header_row, sheetName: sheet_name });
146
+ const blocks = deliverFile(Buffer.from(json, "utf-8"), "application/json", {
147
+ destination_path,
148
+ filename,
149
+ destination_overwrite,
150
+ destination_relay,
151
+ });
152
+ return { content: blocks };
153
+ }
154
+ catch (error) {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ return {
157
+ content: [{ type: "text", text: `Error: ${message}` }],
158
+ isError: true,
159
+ };
160
+ }
161
+ });
135
162
  // ─── main ─────────────────────────────────────────────────────────────────────
136
163
  async function main() {
137
164
  const transport = new StdioServerTransport();
package/dist/relay.js ADDED
@@ -0,0 +1,70 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ // Un fichier produit par ce MCP n'a aucun moyen d'atteindre la machine de l'agent quand
4
+ // le MCP tourne derrière mcp-http-gateway : le protocole n'offre au serveur aucune
5
+ // primitive pour y écrire. La convention du relais règle ça — dès qu'un outil déclare la
6
+ // paire `destination_path` / `destination_relay`, le relais retient la destination, pose
7
+ // `destination_relay`, et écrit lui-même les octets qu'on lui renvoie.
8
+ //
9
+ // Ici, on ne sait pas si un relais est dans la chaîne : `destination_relay` le dit. Posé,
10
+ // on renvoie le contenu ; absent, les deux machines n'en font qu'une et on écrit nous-même.
11
+ /** Plafond de ce qui peut être encodé dans une réponse d'outil. Le relais applique sa
12
+ * propre limite, configurable ; celle-ci n'existe que pour qu'un fichier démesuré
13
+ * échoue proprement ici plutôt qu'en épuisant la mémoire pendant l'encodage base64. */
14
+ const RELAY_MAX_BYTES = 32 * 1024 * 1024;
15
+ export const RELAY_SHAPE_DESCRIPTIONS = {
16
+ destination_path: "ABSOLUTE path of the destination DIRECTORY on the agent machine. It is created if missing, and the file keeps the name given in filename",
17
+ filename: "Name of the file to create, extension included (e.g. 'workbook.json'). Must not contain path separators",
18
+ destination_overwrite: "Replace the destination file if it already exists. Defaults to false",
19
+ destination_relay: "Filled in by the mcp-http-gateway relay, never by the agent. Signals that the destination lives on another machine and that the file content must travel in the response",
20
+ };
21
+ /**
22
+ * Livre un fichier produit par le MCP, du bon côté de la chaîne.
23
+ *
24
+ * Renvoie les blocs de contenu à faire figurer dans le résultat de l'outil : un bloc
25
+ * `resource` que le relais interceptera et retirera, ou une simple confirmation quand
26
+ * l'écriture a eu lieu ici. Toute impossibilité est une erreur franche : mieux vaut un
27
+ * fichier perdu qu'un fichier écrit sur la mauvaise machine.
28
+ */
29
+ export function deliverFile(data, mimeType, target) {
30
+ const name = path.basename(target.filename);
31
+ if (name !== target.filename || name === "." || name === "..") {
32
+ throw new Error(`filename must be a bare file name, without path separators: ${target.filename}`);
33
+ }
34
+ if (data.length > RELAY_MAX_BYTES) {
35
+ throw new Error(`File too large to transfer: ${data.length} bytes, the limit is ${RELAY_MAX_BYTES}.`);
36
+ }
37
+ // Le relais tourne sur la machine de l'agent et a retenu la destination : il attend les
38
+ // octets, qu'il écrira lui-même. Écrire ici viserait le mauvais disque.
39
+ if (target.destination_relay) {
40
+ return [
41
+ {
42
+ type: "resource",
43
+ resource: {
44
+ uri: `file:///${name}`,
45
+ name,
46
+ mimeType,
47
+ blob: data.toString("base64"),
48
+ },
49
+ },
50
+ ];
51
+ }
52
+ // Pas de relais : le MCP tourne à côté de l'agent, son disque est le bon.
53
+ if (!target.destination_path) {
54
+ throw new Error("destination_path is required: the absolute directory where the file is written.");
55
+ }
56
+ if (!path.isAbsolute(target.destination_path)) {
57
+ throw new Error(`destination_path must be an absolute directory path: ${target.destination_path}`);
58
+ }
59
+ if (fs.existsSync(target.destination_path) &&
60
+ !fs.statSync(target.destination_path).isDirectory()) {
61
+ throw new Error(`destination_path must be a directory, but a file already exists there: ${target.destination_path}`);
62
+ }
63
+ const written = path.join(target.destination_path, name);
64
+ if (!target.destination_overwrite && fs.existsSync(written)) {
65
+ throw new Error(`Destination already exists: ${written}. Pass destination_overwrite to replace it.`);
66
+ }
67
+ fs.mkdirSync(target.destination_path, { recursive: true });
68
+ fs.writeFileSync(written, data);
69
+ return [{ type: "text", text: `File written to ${written} (${data.length} bytes).` }];
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/xls-reader",
3
- "version": "2.2.0",
3
+ "version": "2.4.1",
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",