@xlvane/mcp 1.2.2 → 1.2.3
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 +4 -3
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/{server-B1Px_tvv.js → server-Lws8m9Ct.js} +142 -50
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Many MCP clients take a configuration like this one; check your client's documen
|
|
|
29
29
|
| `--max-file-size <bytes>` | Largest file read or written. Default 64 MiB. |
|
|
30
30
|
| `--max-rows <n>` | Largest number of rows one `read_table` call returns. Default 1000. |
|
|
31
31
|
| `--max-response-chars <n>` | Largest tool result (also an error), in characters of its JSON text; the message carries the same data again as `structuredContent`. `read_table` returns the rows that fit (`truncated`, `nextOffset`); other tools return `XLV_E_LIMIT_EXCEEDED`. Default 1 Mi (1 048 576). |
|
|
32
|
+
| `--max-cells <n>` | Largest number of cells in a workbook a tool reads (the `maxCells` read limit). Default 5 000 000. Lower it when the server must answer quickly: `diff` holds both workbooks in memory. |
|
|
32
33
|
|
|
33
34
|
The server speaks MCP over stdin and stdout; messages for people go to stderr.
|
|
34
35
|
|
|
@@ -48,9 +49,9 @@ Every tool has a JSON Schema for its input and returns its result as `structured
|
|
|
48
49
|
|
|
49
50
|
## Security
|
|
50
51
|
|
|
51
|
-
- **Directories:** files are read and written only inside the `--root` directories. Paths are checked after symbolic links are resolved, so `..`, absolute paths and links that lead outside are refused (`XLV_E_PATH_NOT_ALLOWED`). A file is checked again after it is opened (the path must still lead to the same file), so swapping it for a link meanwhile does not help. Writing through a symbolic link is refused. A path outside the roots gets the same answer whether or not something exists there. Only regular files are read.
|
|
52
|
-
- **Read-only by default:** the write tools exist only with `--allow-write`. They create files exclusively and replace an existing file only when the call passes `"overwrite": true`, through a temporary file and a rename; the replacement keeps the file's
|
|
53
|
-
- **Limits:** files above `--max-file-size` are refused for reading and writing, and results above `--max-response-chars` are cut (`read_table`) or refused. The xlvane reader applies its limits for untrusted files (ZIP bombs, XML without DTDs or entities, the budgets of xlvane 1.1.1). `validate` lists at most 1 000 problems (`problemsTotal` counts all).
|
|
52
|
+
- **Directories:** files are read and written only inside the `--root` directories. Paths are checked after symbolic links are resolved, so `..`, absolute paths and links that lead outside are refused (`XLV_E_PATH_NOT_ALLOWED`). A file is checked again after it is opened (the path must still lead to the same file), so swapping it for a link meanwhile does not help. Writing through a symbolic link is refused. A path outside the roots gets the same answer whether or not something exists there, also when it is reached through a symbolic link inside a root. Only regular files are read. Messages on stderr show control characters as `\uXXXX`.
|
|
53
|
+
- **Read-only by default:** the write tools exist only with `--allow-write`. They create files exclusively and replace an existing file only when the call passes `"overwrite": true`, through a temporary file and a rename; the replacement keeps the file's permission bits (not setuid, setgid or sticky), and read-only files are not replaced. `edit_cells` writes formulas as given (for example `HYPERLINK(...)`); they are calculated when a spreadsheet application opens the file. Any file name inside a root may be written, also dot files such as `.gitignore`; choose the roots accordingly.
|
|
54
|
+
- **Limits:** files above `--max-file-size` are refused for reading and writing (`convert` to JSON stops building the text at that size), workbooks above `--max-cells` cells are refused, and results above `--max-response-chars` are cut (`read_table`) or refused. The official MCP SDK closes the connection after a message larger than 10 MiB. The xlvane reader applies its limits for untrusted files (ZIP bombs, XML without DTDs or entities, the budgets of xlvane 1.1.1). `validate` lists at most 1 000 problems (`problemsTotal` counts all).
|
|
54
55
|
- **File content is data:** values, sheet names and other text come from the files and may contain any text, also text meant to influence a model. The server returns it as data and does not interpret it.
|
|
55
56
|
- **No code execution:** macros are never run, external links are never followed, and formulas are not calculated.
|
|
56
57
|
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ interface ServerOptions {
|
|
|
23
23
|
* (`truncated`, `nextOffset`); other tools return `XLV_E_LIMIT_EXCEEDED`. Default 1 Mi.
|
|
24
24
|
*/
|
|
25
25
|
readonly maxResponseChars?: number;
|
|
26
|
+
/** Largest number of cells in a workbook the tools read (the `maxCells` read limit). Default 5 000 000. */
|
|
27
|
+
readonly maxCells?: number;
|
|
26
28
|
/** Directory that relative roots are resolved against. Default: the current directory. */
|
|
27
29
|
readonly cwd?: string;
|
|
28
30
|
}
|
|
@@ -42,7 +44,7 @@ interface ServerOptions {
|
|
|
42
44
|
export declare function createServer(options: ServerOptions): Promise<McpServer>;
|
|
43
45
|
/**
|
|
44
46
|
* Runs the server from the command line: parses `--root`, `--allow-write`, `--max-file-size`,
|
|
45
|
-
* `--max-rows` and `--max-
|
|
47
|
+
* `--max-rows`, `--max-response-chars` and `--max-cells`, then serves MCP over stdio until the client closes stdin. Returns the exit
|
|
46
48
|
* code. `xlvane mcp` calls this function.
|
|
47
49
|
*
|
|
48
50
|
* @example
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createServer, n as runCli } from "./server-
|
|
1
|
+
export { t as createServer, n as runCli } from "./server-Lws8m9Ct.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { McpServer, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
3
3
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
4
|
-
import { ERROR_VALUES, XlvError, appendRows, diff, editCells, fromCSV, fromJSON, inspect, isXlvError, readTable, readWorkbook, toCSV, toJSON, writeTable } from "xlvane";
|
|
4
|
+
import { DEFAULT_READ_LIMITS, ERROR_VALUES, XlvError, appendRows, diff, editCells, fromCSV, fromJSON, inspect, isXlvError, readTable, readWorkbook, toCSV, toJSON, writeTable } from "xlvane";
|
|
5
5
|
import { constants } from "node:fs";
|
|
6
|
-
import {
|
|
6
|
+
import { lstat, open, readlink, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
8
8
|
// src/paths.ts
|
|
9
9
|
function errorCode(error) {
|
|
@@ -47,6 +47,61 @@ function within(dirs, path) {
|
|
|
47
47
|
function inside(roots, real) {
|
|
48
48
|
return within(roots.dirs, real);
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The real path of `target`. When it cannot be resolved (it does not exist, or a file stands where
|
|
52
|
+
* a directory should), the nearest ancestor that exists is resolved instead: if that leads outside
|
|
53
|
+
* the roots, the path is refused as outside, so the answer does not depend on what exists behind a
|
|
54
|
+
* symbolic link inside a root (D-J01). Otherwise `onMissing` gives the error for the missing path.
|
|
55
|
+
*/
|
|
56
|
+
async function realInside(roots, path, target, onMissing) {
|
|
57
|
+
let real;
|
|
58
|
+
try {
|
|
59
|
+
real = await realpath(target);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (await leadsOutside(roots, target, 0)) throw notAllowed(roots, path, "");
|
|
62
|
+
throw onMissing(error);
|
|
63
|
+
}
|
|
64
|
+
if (!inside(roots, real)) throw notAllowed(roots, path, "");
|
|
65
|
+
return real;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a path that does not resolve would lead outside the roots: its nearest existing
|
|
69
|
+
* ancestor is found with `lstat`; a symbolic link there (also one whose target is missing) is
|
|
70
|
+
* followed by its text, anything else by its real path, and the rest of the path is added.
|
|
71
|
+
*/
|
|
72
|
+
async function leadsOutside(roots, path, depth) {
|
|
73
|
+
if (depth > 40) return true;
|
|
74
|
+
let ancestor = path;
|
|
75
|
+
const rest = [];
|
|
76
|
+
for (;;) {
|
|
77
|
+
let info;
|
|
78
|
+
try {
|
|
79
|
+
info = await lstat(ancestor);
|
|
80
|
+
} catch {
|
|
81
|
+
const parent = dirname(ancestor);
|
|
82
|
+
if (parent === ancestor) return false;
|
|
83
|
+
rest.unshift(basename(ancestor));
|
|
84
|
+
ancestor = parent;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (info.isSymbolicLink()) {
|
|
88
|
+
let text;
|
|
89
|
+
try {
|
|
90
|
+
text = await readlink(ancestor);
|
|
91
|
+
} catch {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return leadsOutside(roots, join(resolve(dirname(ancestor), text), ...rest), depth + 1);
|
|
95
|
+
}
|
|
96
|
+
let real;
|
|
97
|
+
try {
|
|
98
|
+
real = await realpath(ancestor);
|
|
99
|
+
} catch {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return !inside(roots, join(real, ...rest));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
50
105
|
function notAllowed(roots, path, why) {
|
|
51
106
|
return new XlvError("XLV_E_PATH_NOT_ALLOWED", `"${path}" is outside the directories this server may use${why}.`, {
|
|
52
107
|
hint: `Use a path inside ${roots.dirs.map((d) => `"${d}"`).join(" or ")}. Relative paths are resolved against the first one.`,
|
|
@@ -86,22 +141,13 @@ function readAllowed(roots, path) {
|
|
|
86
141
|
*/
|
|
87
142
|
async function readAllowedWith(roots, path, hooks) {
|
|
88
143
|
const target = absolute(roots, path);
|
|
89
|
-
const resolveInside =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw new XlvError("XLV_E_FILE_READ", `Cannot read "${path}" (${errorCode(error)}).`, {
|
|
95
|
-
hint: errorCode(error) === "ENOENT" ? "The file does not exist. Use inspect_workbook on a file that exists, or check the name." : "Check that the path points to a readable file.",
|
|
96
|
-
details: {
|
|
97
|
-
path,
|
|
98
|
-
code: errorCode(error)
|
|
99
|
-
}
|
|
100
|
-
});
|
|
144
|
+
const resolveInside = () => realInside(roots, path, target, (error) => new XlvError("XLV_E_FILE_READ", `Cannot read "${path}" (${errorCode(error)}).`, {
|
|
145
|
+
hint: errorCode(error) === "ENOENT" ? "The file does not exist. Use inspect_workbook on a file that exists, or check the name." : "Check that the path points to a readable file.",
|
|
146
|
+
details: {
|
|
147
|
+
path,
|
|
148
|
+
code: errorCode(error)
|
|
101
149
|
}
|
|
102
|
-
|
|
103
|
-
return real;
|
|
104
|
-
};
|
|
150
|
+
}));
|
|
105
151
|
const notAFile = () => new XlvError("XLV_E_FILE_READ", `"${path}" is not a file.`, {
|
|
106
152
|
hint: "Pass the path of a file.",
|
|
107
153
|
details: { path }
|
|
@@ -172,19 +218,13 @@ async function writeAllowed(roots, path, data, overwrite) {
|
|
|
172
218
|
hint: "Pass a path that ends in a file name.",
|
|
173
219
|
details: { path }
|
|
174
220
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
path,
|
|
183
|
-
code: errorCode(error)
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
if (!inside(roots, dir)) throw notAllowed(roots, path, dir === dirname(target) ? "" : " (after following symbolic links)");
|
|
221
|
+
const dir = await realInside(roots, path, dirname(target), (error) => new XlvError("XLV_E_FILE_WRITE", `The directory of "${path}" does not exist (${errorCode(error)}).`, {
|
|
222
|
+
hint: "Write into an existing directory inside the allowed roots.",
|
|
223
|
+
details: {
|
|
224
|
+
path,
|
|
225
|
+
code: errorCode(error)
|
|
226
|
+
}
|
|
227
|
+
}));
|
|
188
228
|
const file = join(dir, name);
|
|
189
229
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
190
230
|
if (bytes.length > roots.maxFileBytes) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The output would be ${String(bytes.length)} bytes, more than the server's limit of ${String(roots.maxFileBytes)}.`, {
|
|
@@ -247,9 +287,14 @@ async function writeAllowed(roots, path, data, overwrite) {
|
|
|
247
287
|
const temp = join(dir, `.${name}.xlvane-${random}.tmp`);
|
|
248
288
|
let created = false;
|
|
249
289
|
try {
|
|
250
|
-
await
|
|
290
|
+
const handle = await open(temp, "wx", 384);
|
|
251
291
|
created = true;
|
|
252
|
-
|
|
292
|
+
try {
|
|
293
|
+
await handle.writeFile(bytes);
|
|
294
|
+
await handle.chmod(existing.mode & 511);
|
|
295
|
+
} finally {
|
|
296
|
+
await handle.close();
|
|
297
|
+
}
|
|
253
298
|
await rename(temp, file);
|
|
254
299
|
} catch (error) {
|
|
255
300
|
if (created) await rm(temp, { force: true });
|
|
@@ -419,14 +464,39 @@ function handler(maxChars, run) {
|
|
|
419
464
|
}
|
|
420
465
|
};
|
|
421
466
|
}
|
|
422
|
-
async function book(roots, path) {
|
|
467
|
+
async function book(roots, limits, path) {
|
|
423
468
|
const bytes = await readAllowed(roots, path);
|
|
424
469
|
try {
|
|
425
|
-
return readWorkbook(bytes);
|
|
470
|
+
return readWorkbook(bytes, { limits: { maxCells: limits.maxCells } });
|
|
426
471
|
} catch (error) {
|
|
427
472
|
throw withPath(error, path);
|
|
428
473
|
}
|
|
429
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* `JSON.stringify(rows, null, 2)` plus a line feed, built row by row: every row repeats the header
|
|
477
|
+
* names, so a small file can make a text far larger than the file limit, or than a string can be.
|
|
478
|
+
* The text stops as soon as it is longer than `maxChars` characters, before encoding (D-J01).
|
|
479
|
+
*/
|
|
480
|
+
function jsonText(rows, maxChars, path) {
|
|
481
|
+
if (rows.length === 0) return "[]\n";
|
|
482
|
+
const parts = ["["];
|
|
483
|
+
let length = 1;
|
|
484
|
+
for (let i = 0; i < rows.length; i++) {
|
|
485
|
+
const part = `${i === 0 ? "" : ","}\n ${JSON.stringify(rows[i], null, 2).replace(/\n/g, "\n ")}`;
|
|
486
|
+
length += part.length;
|
|
487
|
+
if (length > maxChars) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The JSON text would be longer than the server's limit of ${String(maxChars)} bytes.`, {
|
|
488
|
+
hint: "Convert fewer rows or columns (sheet with a smaller table), or raise --max-file-size.",
|
|
489
|
+
details: {
|
|
490
|
+
path,
|
|
491
|
+
limit: "maxFileBytes",
|
|
492
|
+
value: maxChars
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
parts.push(part);
|
|
496
|
+
}
|
|
497
|
+
parts.push("\n]\n");
|
|
498
|
+
return parts.join("");
|
|
499
|
+
}
|
|
430
500
|
function withPath(error, path) {
|
|
431
501
|
if (error instanceof XlvError && error.details["path"] === void 0) return new XlvError(error.code, error.message, {
|
|
432
502
|
location: error.location,
|
|
@@ -503,7 +573,7 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
503
573
|
}, ["path"]),
|
|
504
574
|
annotations: READ
|
|
505
575
|
}, handler(limits.maxResponseChars, async (a) => {
|
|
506
|
-
const workbook = await book(roots, a["path"]);
|
|
576
|
+
const workbook = await book(roots, limits, a["path"]);
|
|
507
577
|
const report = inspect(workbook);
|
|
508
578
|
if (typeof a["sheet"] !== "string") return report;
|
|
509
579
|
const sheet = workbook.getSheet(a["sheet"]);
|
|
@@ -538,7 +608,7 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
538
608
|
}, ["path"]),
|
|
539
609
|
annotations: READ
|
|
540
610
|
}, handler(limits.maxResponseChars, async (a) => {
|
|
541
|
-
const workbook = await book(roots, a["path"]);
|
|
611
|
+
const workbook = await book(roots, limits, a["path"]);
|
|
542
612
|
const table = readTable(workbook, {
|
|
543
613
|
...typeof a["sheet"] === "string" ? { sheet: a["sheet"] } : {},
|
|
544
614
|
...typeof a["range"] === "string" ? { range: a["range"] } : {},
|
|
@@ -610,8 +680,8 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
610
680
|
}, ["before", "after"]),
|
|
611
681
|
annotations: READ
|
|
612
682
|
}, handler(limits.maxResponseChars, async (a) => {
|
|
613
|
-
const before = await book(roots, a["before"]);
|
|
614
|
-
const after = await book(roots, a["after"]);
|
|
683
|
+
const before = await book(roots, limits, a["before"]);
|
|
684
|
+
const after = await book(roots, limits, a["after"]);
|
|
615
685
|
const r = diff(before, after, { styles: a["styles"] === true });
|
|
616
686
|
const max = a["maxChanges"] ?? 200;
|
|
617
687
|
return {
|
|
@@ -655,7 +725,7 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
655
725
|
}, ["path"]),
|
|
656
726
|
annotations: READ
|
|
657
727
|
}, handler(limits.maxResponseChars, async (a) => {
|
|
658
|
-
const workbook = await book(roots, a["path"]);
|
|
728
|
+
const workbook = await book(roots, limits, a["path"]);
|
|
659
729
|
const warnings = inspect(workbook).warnings;
|
|
660
730
|
const rules = a["columns"] ?? [];
|
|
661
731
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -827,13 +897,16 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
827
897
|
sheet: e.sheet,
|
|
828
898
|
cell: e.cell.toUpperCase(),
|
|
829
899
|
value: cellInput(e.value)
|
|
830
|
-
})));
|
|
900
|
+
})), { limits: { maxCells: limits.maxCells } });
|
|
831
901
|
bytes = r.bytes;
|
|
832
902
|
warnings.push(...r.warnings);
|
|
833
903
|
}
|
|
834
904
|
if (append) {
|
|
835
905
|
const rows = append.rows.map((r) => Array.isArray(r) ? r.map(cellInput) : Object.fromEntries(Object.entries(r).map(([k, v]) => [k, cellInput(v)])));
|
|
836
|
-
const r = appendRows(bytes, append.sheet, rows,
|
|
906
|
+
const r = appendRows(bytes, append.sheet, rows, {
|
|
907
|
+
limits: { maxCells: limits.maxCells },
|
|
908
|
+
...append.header !== void 0 ? { header: append.header } : {}
|
|
909
|
+
});
|
|
837
910
|
bytes = r.bytes;
|
|
838
911
|
warnings.push(...r.warnings);
|
|
839
912
|
}
|
|
@@ -880,8 +953,8 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
880
953
|
const sheet = typeof a["sheet"] === "string" ? { sheet: a["sheet"] } : {};
|
|
881
954
|
let data;
|
|
882
955
|
if (books.includes(from) && tables.includes(to)) {
|
|
883
|
-
const workbook = await book(roots, input);
|
|
884
|
-
data = to === "json" ?
|
|
956
|
+
const workbook = await book(roots, limits, input);
|
|
957
|
+
data = to === "json" ? jsonText(toJSON(workbook, sheet).rows, roots.maxFileBytes, output) : toCSV(workbook, {
|
|
885
958
|
...sheet,
|
|
886
959
|
delimiter: a["delimiter"] ?? (to === "tsv" ? " " : ",")
|
|
887
960
|
});
|
|
@@ -899,10 +972,12 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
899
972
|
try {
|
|
900
973
|
data = from === "json" ? fromJSON(text, {
|
|
901
974
|
...sheet,
|
|
902
|
-
preset: "plain"
|
|
975
|
+
preset: "plain",
|
|
976
|
+
maxCells: limits.maxCells
|
|
903
977
|
}) : fromCSV(text, {
|
|
904
978
|
...sheet,
|
|
905
|
-
delimiter: a["delimiter"] ?? (from === "tsv" ? " " : ",")
|
|
979
|
+
delimiter: a["delimiter"] ?? (from === "tsv" ? " " : ","),
|
|
980
|
+
maxCells: limits.maxCells
|
|
906
981
|
});
|
|
907
982
|
} catch (error) {
|
|
908
983
|
throw withPath(error, input);
|
|
@@ -923,7 +998,7 @@ function registerTools(server, roots, allowWrite, limits) {
|
|
|
923
998
|
}
|
|
924
999
|
// src/server.ts
|
|
925
1000
|
/** Version reported in server info; kept equal to package.json by tests/mcp/server.test.ts. */
|
|
926
|
-
const SERVER_VERSION = "1.2.
|
|
1001
|
+
const SERVER_VERSION = "1.2.3";
|
|
927
1002
|
/** Default largest file read or written by the tools: 64 MiB. */
|
|
928
1003
|
const DEFAULT_MAX_FILE_BYTES = 67108864;
|
|
929
1004
|
/** Default largest number of rows returned by one read_table call. */
|
|
@@ -954,7 +1029,8 @@ function build(roots, allowWrite, limits) {
|
|
|
954
1029
|
async function prepare(options) {
|
|
955
1030
|
const limits = {
|
|
956
1031
|
maxRows: options.maxRows ?? 1e3,
|
|
957
|
-
maxResponseChars: options.maxResponseChars ?? 1048576
|
|
1032
|
+
maxResponseChars: options.maxResponseChars ?? 1048576,
|
|
1033
|
+
maxCells: options.maxCells ?? DEFAULT_READ_LIMITS.maxCells
|
|
958
1034
|
};
|
|
959
1035
|
return {
|
|
960
1036
|
roots: await resolveRoots(options.roots, options.cwd ?? ".", options.maxFileBytes ?? 67108864),
|
|
@@ -992,6 +1068,7 @@ Options:
|
|
|
992
1068
|
--max-rows <n> Largest number of rows per read_table call (default ${String(DEFAULT_MAX_ROWS)})
|
|
993
1069
|
--max-response-chars <n>
|
|
994
1070
|
Largest tool result in characters of JSON (default ${String(DEFAULT_MAX_RESPONSE_CHARS)})
|
|
1071
|
+
--max-cells <n> Largest number of cells in a workbook read (default ${String(DEFAULT_READ_LIMITS.maxCells)})
|
|
995
1072
|
-h, --help Show this help
|
|
996
1073
|
|
|
997
1074
|
The server speaks MCP over stdin and stdout; messages for people go to stderr.
|
|
@@ -1002,8 +1079,20 @@ function positive(value, name) {
|
|
|
1002
1079
|
if (!/^\d+$/.test(value) || Number(value) < 1) throw new Error(`--${name} must be a whole number of 1 or more, not "${value}".`);
|
|
1003
1080
|
return Number(value);
|
|
1004
1081
|
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Text for stderr with control characters (other than line feeds), C1 controls and bidirectional
|
|
1084
|
+
* formatting characters written as `\uXXXX`: arguments and directory names come from the client
|
|
1085
|
+
* configuration and must not reach the terminal as escape sequences (D-J01).
|
|
1086
|
+
*/
|
|
1087
|
+
function printable(text) {
|
|
1088
|
+
return text.replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
1089
|
+
}
|
|
1005
1090
|
/** runCli with its environment passed in (tests). */
|
|
1006
|
-
async function runWith(args,
|
|
1091
|
+
async function runWith(args, rawIO) {
|
|
1092
|
+
const io = {
|
|
1093
|
+
...rawIO,
|
|
1094
|
+
stderr: (text) => rawIO.stderr(printable(text))
|
|
1095
|
+
};
|
|
1007
1096
|
let values;
|
|
1008
1097
|
try {
|
|
1009
1098
|
values = parseArgs({
|
|
@@ -1017,6 +1106,7 @@ async function runWith(args, io) {
|
|
|
1017
1106
|
"max-file-size": { type: "string" },
|
|
1018
1107
|
"max-rows": { type: "string" },
|
|
1019
1108
|
"max-response-chars": { type: "string" },
|
|
1109
|
+
"max-cells": { type: "string" },
|
|
1020
1110
|
help: {
|
|
1021
1111
|
type: "boolean",
|
|
1022
1112
|
short: "h"
|
|
@@ -1044,13 +1134,15 @@ async function runWith(args, io) {
|
|
|
1044
1134
|
const maxFileBytes = positive(values["max-file-size"], "max-file-size");
|
|
1045
1135
|
const maxRows = positive(values["max-rows"], "max-rows");
|
|
1046
1136
|
const maxResponseChars = positive(values["max-response-chars"], "max-response-chars");
|
|
1137
|
+
const maxCells = positive(values["max-cells"], "max-cells");
|
|
1047
1138
|
prepared = await prepare({
|
|
1048
1139
|
roots,
|
|
1049
1140
|
cwd: io.cwd,
|
|
1050
1141
|
allowWrite: values["allow-write"] === true,
|
|
1051
1142
|
...maxFileBytes !== void 0 ? { maxFileBytes } : {},
|
|
1052
1143
|
...maxRows !== void 0 ? { maxRows } : {},
|
|
1053
|
-
...maxResponseChars !== void 0 ? { maxResponseChars } : {}
|
|
1144
|
+
...maxResponseChars !== void 0 ? { maxResponseChars } : {},
|
|
1145
|
+
...maxCells !== void 0 ? { maxCells } : {}
|
|
1054
1146
|
});
|
|
1055
1147
|
} catch (error) {
|
|
1056
1148
|
io.stderr(`xlvane-mcp: ${error instanceof Error ? error.message : String(error)}\n${isXlvError(error) ? `${error.hint}\n` : ""}`);
|
|
@@ -1063,7 +1155,7 @@ async function runWith(args, io) {
|
|
|
1063
1155
|
}
|
|
1064
1156
|
/**
|
|
1065
1157
|
* Runs the server from the command line: parses `--root`, `--allow-write`, `--max-file-size`,
|
|
1066
|
-
* `--max-rows` and `--max-
|
|
1158
|
+
* `--max-rows`, `--max-response-chars` and `--max-cells`, then serves MCP over stdio until the client closes stdin. Returns the exit
|
|
1067
1159
|
* code. `xlvane mcp` calls this function.
|
|
1068
1160
|
*
|
|
1069
1161
|
* @example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xlvane/mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"mcpName": "io.github.DevXord/xlvane",
|
|
5
5
|
"description": "MCP server for XLSX files: inspect, read, write, edit, convert, compare and validate workbooks with xlvane, limited to the directories given at start.",
|
|
6
6
|
"keywords": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/server": "2.4.0",
|
|
48
|
-
"xlvane": "^1.2.
|
|
48
|
+
"xlvane": "^1.2.3"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public",
|