logisheets-mcp 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/README.md +173 -69
  2. package/package.json +7 -5
package/README.md CHANGED
@@ -1,46 +1,65 @@
1
1
  # logisheets-mcp
2
2
 
3
- **A real spreadsheet engine your AI agent can think in.**
3
+ **A real spreadsheet engine your agent can think in.** Excel-compatible formulas
4
+ it doesn't have to do in its head, structured memory it addresses by name, and a
5
+ genuine `.xlsx` at the end that a person can open, audit and keep using.
4
6
 
5
- An [MCP](https://modelcontextprotocol.io) server that gives any LLM agent a
6
- real, Excel-compatible calculation engine — with structured memory it can
7
- address semantically, and a genuine `.xlsx` at the end that a human can open,
8
- audit, and keep using.
7
+ An [MCP](https://modelcontextprotocol.io) server over
8
+ [LogiSheets](https://github.com/logisky/LogiSheets), a spreadsheet engine written
9
+ in Rust. MIT, runs on your machine, opens no sockets.
9
10
 
10
- Built on [LogiSheets](https://github.com/logisky/LogiSheets), a spreadsheet
11
- engine written in Rust. MIT licensed, self-hostable, no cloud dependency.
11
+ ---
12
12
 
13
- ## Why
13
+ Three things an agent is bad at on a grid, and does not have to do here.
14
14
 
15
- Agents are doing real work that is spreadsheet-shaped — financial models, data
16
- reconciliation, analysis — and they are bad at exactly the parts a spreadsheet
17
- engine is good at.
15
+ ### It doesn't do the arithmetic
18
16
 
19
- **Arithmetic.** Agents mis-sum and mis-multiply. Here they don't have to: they
20
- write a formula and a deterministic engine evaluates it.
17
+ ```
18
+ set_field_rule proj.pv = #FIELD("fcf") * #FIELD("df")
19
+ describe_block proj
20
+ → Y1 147.2727 Y2 144.5950 Y3 141.9660 Y4 139.3848 Y5 136.8506
21
+ ```
22
+
23
+ The rule is stated once, **for the field** — not per cell. Five years of it
24
+ materialise, and a sixth computes the moment a row is added, with no formula
25
+ written for it. Writing the same formula N times with the row number adjusted is
26
+ precisely where a model makes a silent mistake.
27
+
28
+ ### It doesn't keep track of where anything is
29
+
30
+ ```
31
+ … the sheet is reshaped: a projected year deleted,
32
+ two rows inserted at the top, a column at the left …
21
33
 
22
- **Memory.** Across a thirty-step task, intermediate state has to live
23
- *somewhere* structured. A context window is lossy and expensive; a code
24
- sandbox's variables vanish. This server gives the agent an external structured
25
- disk it reads and writes across the whole task.
34
+ describe_block val
35
+ → per_share 19.383943 ← still right, nothing re-derived
36
+ ```
37
+
38
+ Same question, same address, after the shape changed underneath it.
39
+ `SUM(BLOCKREFS("proj","*","pv"))` never referred to a position, so the edit had
40
+ nothing to break. A model that spent its attention on bookkeeping — *did my rows
41
+ shift? is my range still right?* — spends none of it here.
26
42
 
27
- **Addressing.** Agents are bad at spatial reasoning, so a raw grid is a fragile
28
- surface — they lose track of where things are, and their own edits break their
29
- references. So the agent doesn't address `C7`. It addresses
30
- **`(block, row_key, field)`**:
43
+ ### It doesn't burn context on round trips
31
44
 
32
- > set the `price` field of the `2025` record in the `revenue` block
45
+ ```
46
+ preview_changes scenarios: WACC × terminal growth, 4 × 4
47
+ → 16 answers, one call, 950 bytes, nothing written to the workbook
33
48
 
34
- Insert a row, move the block, add a column — that address still resolves. This
35
- is the whole point: **memory that survives the agent's own edits.**
49
+ goal_seek per_share = 30, by varying WACC
50
+ → 0.080699, one call
51
+ ```
36
52
 
37
- ### vs. a Python sandbox
53
+ A whole sensitivity table and an inverse solve are single questions, asked on a
54
+ temp branch that is discarded. No loop of write-recalculate-read, and no risk of
55
+ leaving a scenario behind in the model.
38
56
 
39
- A code interpreter can compute, but you get a throwaway script result. Here you
40
- get a real `.xlsx` with **live formulas still in it** — open it in Excel, change
41
- an input, and the model recalculates. It round-trips the human's existing files,
42
- and it runs on your machine, which matters when the data can't leave.
57
+ ---
43
58
 
59
+ **And the file at the end is a real spreadsheet.** Live formulas, not baked
60
+ numbers — open it in Excel, change an input, watch it recompute. Formulas can be
61
+ written out as `BLOCKREF("proj","Y3","pv")` for a person to read, or resolved to
62
+ plain coordinates for Excel to chew on.
44
63
  ## Install
45
64
 
46
65
  Requires Node 20+.
@@ -123,7 +142,7 @@ falls as the list grows, and every description costs context on every turn.
123
142
  | `describe_block` | A block's schema, keys, and (optionally) its current values. |
124
143
  | `eval_formula` | Evaluate an Excel formula and return the value. Nothing is stored. |
125
144
  | `create_block` | Create a named, structured table. First field is the row key. |
126
- | `convert_to_block` | Turn a table that is already in ordinary cells into a block, in place. |
145
+ | `convert_to_block` | Adopt a table that is already in ordinary cells, in place. Reads the field names off the header row and works out which column is the row key. |
127
146
  | `add_block_rows` | Add records — at the end, or `after_key` / `before_key` to place them. |
128
147
  | `delete_block_rows` | Remove records. |
129
148
  | `move_block_row` | Reorder rows, by key. Presentation only: no computed value changes. |
@@ -213,45 +232,6 @@ Because blocks are created *by the agent as it works*, this needs no
213
232
  pre-prepared file — you can point it at a blank workbook or at a spreadsheet
214
233
  someone sent you.
215
234
 
216
- ## Use as a library
217
-
218
- ```ts
219
- import {createServer} from 'logisheets-mcp'
220
- import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
221
-
222
- const {server, session} = createServer({mode: 'full'})
223
- await server.connect(new StreamableHTTPServerTransport(/* … */))
224
- ```
225
-
226
- `createServer` returns the MCP `Server`, the `WorkbookSession`, and the tool map,
227
- so you can host it over any transport or embed it in an agent framework.
228
-
229
- ## Development
230
-
231
- The server is a thin shell over three LogiSheets packages:
232
- [`logisheets-runtime`](https://www.npmjs.com/package/logisheets-runtime) (the
233
- headless engine), `logisheets-logician` (the agent tool definitions), and the
234
- Rust/WASM core. Working on the server alone needs nothing special:
235
-
236
- ```bash
237
- git clone https://github.com/logisky/logisheets-mcp.git
238
- cd logisheets-mcp
239
- npm install
240
- npm test
241
- ```
242
-
243
- Working on the engine at the same time is the other mode. Check out
244
- [LogiSheets](https://github.com/logisky/LogiSheets) as a sibling directory,
245
- build its packages, then:
246
-
247
- ```bash
248
- npm run link:local # re-run after any npm install
249
- ```
250
-
251
- That symlinks the three packages into `node_modules` so local engine changes
252
- take effect without reinstalling. `scripts/release-deps.mjs` puts the registry
253
- ranges back before publishing.
254
-
255
235
  ## Getting the file back
256
236
 
257
237
  `save_workbook` writes a real `.xlsx` and its result carries an MCP
@@ -306,6 +286,130 @@ That is the `logisheets-mcp` binary, which is what an MCP host runs. Using it
306
286
  [as a library](#use-as-a-library) you can attach any transport you like,
307
287
  including an HTTP one — but then the socket is yours, opened deliberately.
308
288
 
289
+ ## Benchmarks
290
+
291
+ The claims above are measured, not asserted. The harness is in
292
+ [`bench/`](bench/): tasks written down and committed *before* any other server
293
+ was looked at ([`bench/TASKS.md`](bench/TASKS.md)), every expected value derived
294
+ independently in Python, so you can re-run it and disagree.
295
+
296
+ Against the two other MCP servers that work on a local `.xlsx` —
297
+ [spreadsheet-kit](https://github.com/PSU3D0/spreadsheet-mcp) 0.11.1, which has
298
+ its own Rust recalc engine, and
299
+ [excel-mcp-server](https://github.com/haris-musa/excel-mcp-server) 0.1.8, the
300
+ most-installed one, on openpyxl:
301
+
302
+ | | this | spreadsheet-kit | excel-mcp-server |
303
+ | --- | --- | --- | --- |
304
+ | Write a formula, read its value | **30** | **30** | `"=SUM(A1:A2)"` |
305
+ | Five-year DCF, value per share | **20.803603** · 15 calls | **20.803603** · 6 calls | formula text |
306
+ | 4×4 sensitivity, 16 answers | **1 call**, 950 B | 16 calls, 1245 B | can't |
307
+ | Solve backwards for an input | **1 call**, 202 B | 18 calls, 1399 B | can't |
308
+ | Reopen it later and explain it | 4 calls, **2.4 kB** | 5 calls, 21 kB | 2 calls, 24 kB |
309
+ | Answer again after the shape changed | **19.383943** | `#VALUE!` | formula text |
310
+ | Keep a handed-over file's features | **8 of 8** | **8 of 8** | **8 of 8** |
311
+
312
+ That last row started at 0 of 8. Writing the task is what found it: a defined
313
+ name, an Excel table, the document's own author and timestamps — everything the
314
+ engine has no opinion about — was being dropped on every save, and the file was
315
+ not even loading. An Excel table now also arrives as a block named after the
316
+ table (`BLOCKREF("Sales","south","q1")` on a file nobody prepared), and goes
317
+ back out as a table whose range follows the block it became.
318
+
319
+ `"=SUM(A1:A2)"` in the third column is not a bug — openpyxl stores formulas
320
+ without evaluating them, so no scenario can be read back and no inverse solve is
321
+ possible. It writes a correct model; it just cannot answer a question about one.
322
+
323
+ spreadsheet-kit is a genuine peer, correct on everything it can attempt, and
324
+ needed fewer calls than we did to build the model — our extra calls declare a
325
+ schema rather than write cells, which is the trade that pays off in the rows
326
+ below it. It also has forks with undo, branching and checkpoints, and
327
+ LibreOffice-backed screenshots, none of which are here.
328
+
329
+ One caveat on the reading row: each server was reading back a file *it* wrote,
330
+ so ours had blocks in it because we put them there. Given a plain spreadsheet
331
+ from a person, `convert_to_block` adopts the table first — reading the field
332
+ names off the header row and working out the key column — and then `BLOCKREFS`
333
+ by name works. The individual cell formulas stay in coordinates, so only half of
334
+ that advantage transfers.
335
+
336
+ Building this turned up defect after defect in our own engine before it said
337
+ anything about anyone else's: a workbook openpyxl wrote failing to load, every
338
+ inline-string label dropped on the way in, adoption freezing the model it
339
+ adopted into static numbers. Every one presented as a wrong answer reported as a
340
+ success — never as a crash — and fuzzing had found none of them.
341
+
342
+ ## Use as a library
343
+
344
+ ```ts
345
+ import {createServer} from 'logisheets-mcp'
346
+ import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
347
+
348
+ const {server, session} = createServer({mode: 'full'})
349
+ await server.connect(new StreamableHTTPServerTransport(/* … */))
350
+ ```
351
+
352
+ `createServer` returns the MCP `Server`, the `WorkbookSession`, and the tool map,
353
+ so you can host it over any transport or embed it in an agent framework.
354
+
355
+ ## Development
356
+
357
+ The server is a thin shell over three LogiSheets packages:
358
+ [`logisheets-runtime`](https://www.npmjs.com/package/logisheets-runtime) (the
359
+ headless engine), `logisheets-logician` (the agent tool definitions), and the
360
+ Rust/WASM core. Working on the server alone needs nothing special:
361
+
362
+ ```bash
363
+ git clone https://github.com/logisky/logisheets-mcp.git
364
+ cd logisheets-mcp
365
+ npm install
366
+ npm test
367
+ ```
368
+
369
+ Working on the engine at the same time is the other mode. Check out
370
+ [LogiSheets](https://github.com/logisky/LogiSheets) as a sibling directory,
371
+ build its packages, then:
372
+
373
+ ```bash
374
+ npm run link:local # re-run after any npm install
375
+ ```
376
+
377
+ That symlinks the three packages into `node_modules` so local engine changes
378
+ take effect without reinstalling. `scripts/release-deps.mjs` puts the registry
379
+ ranges back before publishing.
380
+
381
+ ### Releasing
382
+
383
+ A tag does it. `.github/workflows/publish.yaml` runs the tests, publishes to
384
+ npm with provenance, and registers the new version with the MCP Registry:
385
+
386
+ ```bash
387
+ npm version 0.2.0 # bumps both files, commits, tags v0.2.0
388
+ npm run check-release # optional; CI runs it too
389
+ git push --follow-tags
390
+ ```
391
+
392
+ The workflow can also be run by hand from the Actions tab, which takes the
393
+ version from `package.json` instead of a tag. The npm step skips a version that
394
+ is already published, so a run that failed at the registry step can just be
395
+ re-run — the two publishes are not a transaction.
396
+
397
+ `npm version` also rewrites `server.json`, via the `version` lifecycle script.
398
+ The registry keeps the version in two places — the server's own `version` and
399
+ the version of the npm package it points at — and hand-editing them is the step
400
+ most likely to be missed.
401
+
402
+ `check-release` is the gate. Four things have to agree: the tag, `package.json`,
403
+ and both `server.json` version fields. `mcpName` also has to equal
404
+ `server.json`'s `name`, because the registry proves ownership by reading
405
+ `mcpName` out of the *published* npm package. `npm publish` cannot be undone —
406
+ a version number is spent the moment it lands — so the workflow runs this check
407
+ before publishing, not after.
408
+
409
+ Registry auth needs no secret: the workflow authenticates with GitHub OIDC,
410
+ which is what grants the `io.github.logisky/` namespace. The one secret is
411
+ `NPM_TOKEN`.
412
+
309
413
  ## License
310
414
 
311
415
  MIT. Part of the [LogiSheets](https://github.com/logisky/LogiSheets) project.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "An MCP server that gives an AI agent a real, Excel-compatible spreadsheet engine to compute in and remember in — deterministic formulas, structured block memory addressed by (block, key, field), and a real .xlsx out.",
5
5
  "mcpName": "io.github.logisky/logisheets-mcp",
6
6
  "type": "module",
@@ -23,15 +23,17 @@
23
23
  "typecheck": "tsc --noEmit",
24
24
  "test": "vitest run",
25
25
  "link:local": "node scripts/link-local.mjs",
26
- "prepack": "yarn build",
26
+ "prepack": "npm run build",
27
27
  "release:deps": "node scripts/release-deps.mjs",
28
28
  "demo": "node examples/revenue-model.mjs",
29
- "pretest": "yarn build"
29
+ "pretest": "npm run build",
30
+ "check-release": "node scripts/check-release.mjs",
31
+ "version": "node scripts/sync-server-json.mjs && git add server.json"
30
32
  },
31
33
  "dependencies": {
32
34
  "@modelcontextprotocol/sdk": "^1.30.0",
33
- "logisheets-logician": "^1.12.0",
34
- "logisheets-runtime": "^1.12.0"
35
+ "logisheets-logician": "^1.13.0",
36
+ "logisheets-runtime": "^1.13.0"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@types/node": "^22",