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.
- package/README.md +173 -69
- 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
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
engine written in Rust. MIT licensed, self-hostable, no cloud dependency.
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Three things an agent is bad at on a grid, and does not have to do here.
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
49
|
+
goal_seek per_share = 30, by varying WACC
|
|
50
|
+
→ 0.080699, one call
|
|
51
|
+
```
|
|
36
52
|
|
|
37
|
-
|
|
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
|
-
|
|
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` |
|
|
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.
|
|
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": "
|
|
26
|
+
"prepack": "npm run build",
|
|
27
27
|
"release:deps": "node scripts/release-deps.mjs",
|
|
28
28
|
"demo": "node examples/revenue-model.mjs",
|
|
29
|
-
"pretest": "
|
|
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.
|
|
34
|
-
"logisheets-runtime": "^1.
|
|
35
|
+
"logisheets-logician": "^1.13.0",
|
|
36
|
+
"logisheets-runtime": "^1.13.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@types/node": "^22",
|