@xlvane/mcp 1.2.1 → 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 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 permissions, and read-only files are not replaced. Any file name inside a root may be written, also dot files such as `.gitignore`; choose the roots accordingly.
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as runCli } from "./server-XuY-Hgpe.js";
2
+ import { n as runCli } from "./server-Lws8m9Ct.js";
3
3
  import process from "node:process";
4
4
  // src/bin.ts
5
5
  runCli(process.argv.slice(2)).then((code) => {
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-response-chars`, then serves MCP over stdio until the client closes stdin. Returns the exit
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-XuY-Hgpe.js";
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 { chmod, lstat, open, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
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 = async () => {
90
- let real;
91
- try {
92
- real = await realpath(target);
93
- } catch (error) {
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
- if (!inside(roots, real)) throw notAllowed(roots, path, real === target ? "" : " (after following symbolic links)");
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
- let dir;
176
- try {
177
- dir = await realpath(dirname(target));
178
- } catch (error) {
179
- throw new XlvError("XLV_E_FILE_WRITE", `The directory of "${path}" does not exist (${errorCode(error)}).`, {
180
- hint: "Write into an existing directory inside the allowed roots.",
181
- details: {
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 writeFile(temp, bytes, { flag: "wx" });
290
+ const handle = await open(temp, "wx", 384);
251
291
  created = true;
252
- await chmod(temp, existing.mode & 4095);
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, append.header !== void 0 ? { header: append.header } : void 0);
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" ? `${JSON.stringify(toJSON(workbook, sheet).rows, null, 2)}\n` : toCSV(workbook, {
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.1";
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, io) {
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-response-chars`, then serves MCP over stdio until the client closes stdin. Returns the exit
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@xlvane/mcp",
3
- "version": "1.2.1",
4
- "mcpName": "io.github.devxord/xlvane",
3
+ "version": "1.2.3",
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": [
7
7
  "xlsx",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/server": "2.4.0",
48
- "xlvane": "^1.2.1"
48
+ "xlvane": "^1.2.3"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public",