logisheets-mcp
以下摘录自该仓库的 README(片段) · 在 GitHub 读完整 README →
**A real spreadsheet engine your agent can think in.** Excel-compatible formulas
it doesn't have to do in its head, a table it addresses by name instead of by
coordinate, and a genuine .xlsx at the end that a person can open, audit and
keep using.
An MCP server over
LogiSheets, a spreadsheet engine written
in Rust. MIT, runs on your machine, opens no sockets.
The trouble with a grid
Ask a model for a five-year projection and it writes twenty formulas, each with
the row number adjusted by hand. That is where the silent mistake lives: one of
them reads B7 where it meant B8, the total looks plausible, and nothing
raises an error.
Then the sheet moves. Someone inserts a row at the top, deletes a year, adds a
column. Every coordinate the model was holding is now off by one and it has no
way to notice, so it spends the next turns re-reading cells to work out where
things went instead of on the question you asked.
And every "what if" costs a round trip — write the input, recalculate, read the
output, put it back. Sixteen scenarios is sixteen of those, and a scan that dies
half way leaves a scenario behind in your model.
Blocks
A **block** is a named table on the sheet. Rows have keys, columns have names,
and everything is addressed by those rather than by position.
- **A field's formula is stated once, for the whole column** — not per cell. Add
a row and it computes. There is no twentieth formula to get wrong.
- **A reference names what it means**: *the pv field of the row keyed Y3*.
Insert a row above it and the reference still says the same thing, because it
never said "row 8".
- **The engine owns computed values.** A formula field cannot be overwritten with
a number the model worked out itself.
Blocks are created by the agent as it works, so nothing needs preparing. Point it
at a blank workbook or at a spreadsheet someone emailed you — convert_to_block
adopts a table that is already in ordinary cells, reading the field names off the
header row and working out which column is the key.
The second session
The conversation that builds a spreadsheet is almost never the conversation that
has to answer a question about it. A week later there is a new session, with
none of the context, holding only the file — and what the file records is what
that session can know.
A grid records coordinates. =B11*$B$3*(1-$B$4) is correct and means nothing
until the agent fetches the label column and *infers* that A3 describes B3.
The schema is where the meaning goes instead, and it is written into the
.xlsx: field names, the key column, which fields the engine computes, and the
rule behind each one. One list_blocks call and the workbook introduces itself;
one describe_block and the rules come back as
FIELD("revenue")*BLOCKREF("assum","margin","v") — an explanation rather than
a second lookup problem.
The schema still only says what *shape* the records are. What they **mean** is
prose, so a block carries a description: a sentence or two on create_block, or
set_block_description for a table adopted later, saying what one row is, what
the non-obvious fields hold, and what a later reader must not touch. It is
stored in the file and comes back from describe_block, which is the difference
between the next session reading the intent and inferring it from column names.
src/cold-read.test.ts pins that down rather than
asserting it. It builds a model in one session, saves it, and reopens the file
in a second session sharing nothing with the first — own server, own workbook,
no memory. Then: list_blocks recovers every block's fields, key field,
computed fields and row count in one call; every returned rule is checked to
contain #FIELD or BLOCKREF and **no A1 coordinate at all**; the fresh
session writes a BLOCKREF formula from orientation alone and the engine agrees
with arithmetic done independently in the test; and trace names what reads an
assumption before anyone edits it. Cost is metered on the wire, over the same
text a host shows the model: **540 B for a five-row model, 545 B for a
hundred-and-five-row one**, one call each. Reading a schema is O(columns);