@imenam/xls-reader 2.2.0 → 2.4.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 +164 -145
- package/dist/index.js +55 -33
- package/dist/relay.js +67 -0
- package/package.json +1 -1
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` |
|
|
44
|
-
| `format` | `"markdown" \| "json"
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- **Empty
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ "Nom": "
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- `
|
|
78
|
-
- With `header_row:
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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`. 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
|
-
"
|
|
67
|
-
"Data is limited by a cell budget (cols × rows, default 2000)
|
|
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
|
-
.
|
|
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"
|
|
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("
|
|
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,
|
|
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,54 @@ 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
|
+
destination_path: z.string().describe(RELAY_SHAPE_DESCRIPTIONS.destination_path),
|
|
129
|
+
filename: z.string().describe(RELAY_SHAPE_DESCRIPTIONS.filename),
|
|
130
|
+
destination_overwrite: z
|
|
131
|
+
.boolean()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(RELAY_SHAPE_DESCRIPTIONS.destination_overwrite),
|
|
134
|
+
destination_relay: z
|
|
135
|
+
.boolean()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe(RELAY_SHAPE_DESCRIPTIONS.destination_relay),
|
|
138
|
+
}, async ({ sheet_name, header_row, destination_path, filename, destination_overwrite, destination_relay, ...rest }) => {
|
|
139
|
+
try {
|
|
140
|
+
const json = readWorkbook(toSource(rest), { headerRow: header_row, sheetName: sheet_name });
|
|
141
|
+
const blocks = deliverFile(Buffer.from(json, "utf-8"), "application/json", {
|
|
142
|
+
destination_path,
|
|
143
|
+
filename,
|
|
144
|
+
destination_overwrite,
|
|
145
|
+
destination_relay,
|
|
146
|
+
});
|
|
147
|
+
return { content: blocks };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
135
157
|
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
136
158
|
async function main() {
|
|
137
159
|
const transport = new StdioServerTransport();
|
package/dist/relay.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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 (!path.isAbsolute(target.destination_path)) {
|
|
54
|
+
throw new Error(`destination_path must be an absolute directory path: ${target.destination_path}`);
|
|
55
|
+
}
|
|
56
|
+
if (fs.existsSync(target.destination_path) &&
|
|
57
|
+
!fs.statSync(target.destination_path).isDirectory()) {
|
|
58
|
+
throw new Error(`destination_path must be a directory, but a file already exists there: ${target.destination_path}`);
|
|
59
|
+
}
|
|
60
|
+
const written = path.join(target.destination_path, name);
|
|
61
|
+
if (!target.destination_overwrite && fs.existsSync(written)) {
|
|
62
|
+
throw new Error(`Destination already exists: ${written}. Pass destination_overwrite to replace it.`);
|
|
63
|
+
}
|
|
64
|
+
fs.mkdirSync(target.destination_path, { recursive: true });
|
|
65
|
+
fs.writeFileSync(written, data);
|
|
66
|
+
return [{ type: "text", text: `File written to ${written} (${data.length} bytes).` }];
|
|
67
|
+
}
|
package/package.json
CHANGED