logisheets-mcp 0.1.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/LICENSE +21 -0
- package/README.md +311 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +43 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lifecycle.d.ts +42 -0
- package/dist/lifecycle.js +156 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.js +205 -0
- package/dist/session.d.ts +99 -0
- package/dist/session.js +164 -0
- package/dist/surface.d.ts +24 -0
- package/dist/surface.js +147 -0
- package/dist/validate.d.ts +27 -0
- package/dist/validate.js +248 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeremy He
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# logisheets-mcp
|
|
2
|
+
|
|
3
|
+
**A real spreadsheet engine your AI agent can think in.**
|
|
4
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
Built on [LogiSheets](https://github.com/logisky/LogiSheets), a spreadsheet
|
|
11
|
+
engine written in Rust. MIT licensed, self-hostable, no cloud dependency.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
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.
|
|
18
|
+
|
|
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.
|
|
21
|
+
|
|
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.
|
|
26
|
+
|
|
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)`**:
|
|
31
|
+
|
|
32
|
+
> set the `price` field of the `2025` record in the `revenue` block
|
|
33
|
+
|
|
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.**
|
|
36
|
+
|
|
37
|
+
### vs. a Python sandbox
|
|
38
|
+
|
|
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.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
Requires Node 20+.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g logisheets-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Claude Desktop
|
|
53
|
+
|
|
54
|
+
Add to `claude_desktop_config.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"logisheets": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "logisheets-mcp"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
On macOS that file lives at
|
|
68
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`; on Windows,
|
|
69
|
+
`%APPDATA%\Claude\claude_desktop_config.json`. Restart Claude Desktop
|
|
70
|
+
afterwards.
|
|
71
|
+
|
|
72
|
+
### Cursor / Cline / other hosts
|
|
73
|
+
|
|
74
|
+
Any MCP host that can spawn a stdio server works — point it at the
|
|
75
|
+
`logisheets-mcp` command. For Cursor, add the same block to
|
|
76
|
+
`~/.cursor/mcp.json`.
|
|
77
|
+
|
|
78
|
+
## Try it
|
|
79
|
+
|
|
80
|
+
> Build me a three-year revenue model: 100 units at $9.50 growing 40% a year,
|
|
81
|
+
> with a 30% cost of goods. Then save it to ~/model.xlsx.
|
|
82
|
+
|
|
83
|
+
The agent creates a block, fills it, writes the formulas, and hands back a file.
|
|
84
|
+
The numbers are the engine's, not the model's guesses — and the `.xlsx` has real
|
|
85
|
+
formulas in it, so you can change an assumption in Excel and watch it recompute.
|
|
86
|
+
|
|
87
|
+
## See it work
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm run build && npm run demo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Builds a small revenue model over real MCP-on-stdio against `dist/cli.js` — the
|
|
94
|
+
same code path Claude Desktop drives — and checks every claim as it goes: totals
|
|
95
|
+
the engine computed, a rule that reaches rows added later, blocks that keep
|
|
96
|
+
resolving after the model grows underneath them, and a real `.xlsx` whose
|
|
97
|
+
formulas are verified by reading the file's own bytes. No LLM is involved; the
|
|
98
|
+
engine is the subject, and hard-coding the calls is what makes the guarantees
|
|
99
|
+
checkable rather than a story about a chat session.
|
|
100
|
+
|
|
101
|
+
## The agent loop
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
list_blocks orient: what do I have?
|
|
105
|
+
create_block open a structured workspace
|
|
106
|
+
add_block_rows / set_block_cells fill it, addressed by (block, key, field)
|
|
107
|
+
eval_formula / a stored formula the engine does the math
|
|
108
|
+
describe_block read structured results back
|
|
109
|
+
save_workbook hand the human a real .xlsx
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Tools
|
|
113
|
+
|
|
114
|
+
The default surface is deliberately small — 20 tools. Tool-selection accuracy
|
|
115
|
+
falls as the list grows, and every description costs context on every turn.
|
|
116
|
+
|
|
117
|
+
| Tool | What it does |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `open_workbook` | Start a fresh workbook, or load an existing `.xlsx` from disk. Optional — one appears on first use. |
|
|
120
|
+
| `save_workbook` | Write to a real `.xlsx` file. This is how work gets handed back. |
|
|
121
|
+
| `export_xlsx` | The file as base64, for hosts with no shared filesystem. |
|
|
122
|
+
| `list_blocks` | Every sheet and block, plus where the next block should go. |
|
|
123
|
+
| `describe_block` | A block's schema, keys, and (optionally) its current values. |
|
|
124
|
+
| `eval_formula` | Evaluate an Excel formula and return the value. Nothing is stored. |
|
|
125
|
+
| `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. |
|
|
127
|
+
| `add_block_rows` | Add records — at the end, or `after_key` / `before_key` to place them. |
|
|
128
|
+
| `delete_block_rows` | Remove records. |
|
|
129
|
+
| `move_block_row` | Reorder rows, by key. Presentation only: no computed value changes. |
|
|
130
|
+
| `set_block_cells` | Write cells by `(block, row_key, field)`. Batched, atomic. |
|
|
131
|
+
| `set_field_rule` | Give a field a formula, a validation rule, or an editability rule. |
|
|
132
|
+
| `list_violations` | Which cells break their field's validation rule, and why. |
|
|
133
|
+
| `preview_changes` | What edits *would* do, without doing them. One hypothetical, or a whole grid of scenarios in a single call. |
|
|
134
|
+
| `trace` | What a cell reads, and what reads it — from the engine's dependency graph. |
|
|
135
|
+
| `goal_seek` | What input makes a chosen output equal a target. Searches inside the engine; changes nothing. |
|
|
136
|
+
| `create_sheet` | Add a sheet. |
|
|
137
|
+
| `get_cells` / `set_cells` | Raw-cell escape hatch for data with no structure. |
|
|
138
|
+
|
|
139
|
+
Formulas are Excel-compatible, plus `BLOCKREF(block, key, field)` for reading a
|
|
140
|
+
block cell semantically. Inside a field rule, `#FIELD("name")` is the same row's
|
|
141
|
+
sibling and `#FIELD("name", "key")` is another row of the same block — the one
|
|
142
|
+
carrying that key, never a positional offset, so reordering rows cannot change
|
|
143
|
+
what a formula means.
|
|
144
|
+
|
|
145
|
+
### Analysing a model, not just building one
|
|
146
|
+
|
|
147
|
+
`preview_changes` takes a list of `scenarios` and an optional `watch`, which is
|
|
148
|
+
what turns exploration from dozens of round trips into one:
|
|
149
|
+
|
|
150
|
+
```jsonc
|
|
151
|
+
{
|
|
152
|
+
"scenarios": [
|
|
153
|
+
{"label": "wacc 9%", "changes": [{"block":"assum","row_key":"wacc","field":"v","value":0.09}]},
|
|
154
|
+
{"label": "wacc 12%", "changes": [{"block":"assum","row_key":"wacc","field":"v","value":0.12}]}
|
|
155
|
+
],
|
|
156
|
+
"watch": [{"block":"valuation","row_key":"per_share","field":"v"}]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each scenario runs on its own temp branch and is discarded, so the live model is
|
|
161
|
+
never touched — no mutate-and-revert, and nothing left behind if a scan fails
|
|
162
|
+
half way. A 4×4 sensitivity grid is one call returning sixteen numbers.
|
|
163
|
+
|
|
164
|
+
`goal_seek` runs the same trick backwards — "what discount rate gives a value per
|
|
165
|
+
share of 30" — with the search inside the engine rather than as a conversation,
|
|
166
|
+
so it is one call instead of one per bisection step. It says when a target is
|
|
167
|
+
simply not reachable in the bracket instead of returning the nearest number it
|
|
168
|
+
happened to stop on.
|
|
169
|
+
|
|
170
|
+
`trace` answers the two audit questions from the engine's dependency graph:
|
|
171
|
+
what a cell reads, and what reads it. The second one is why it exists — formula
|
|
172
|
+
text can be read forwards but not backwards, and "what breaks if I change this"
|
|
173
|
+
is the question you want before touching an assumption.
|
|
174
|
+
|
|
175
|
+
Reading a model is semantic too: `describe_block` returns each field's rule, so
|
|
176
|
+
an agent learns the model's logic without visiting a cell, and formulas come back
|
|
177
|
+
naming what they read (`B24 / BLOCKREF("assum","shares","v")`) rather than as
|
|
178
|
+
coordinate chains you have to chase.
|
|
179
|
+
|
|
180
|
+
### The full surface
|
|
181
|
+
|
|
182
|
+
Set `LOGISHEETS_MCP_TOOLS=full` for 50 tools: undo/redo, cell formatting,
|
|
183
|
+
merges, comments, checkpoints, block move/resize, cross-block links, and raw
|
|
184
|
+
row/column structure.
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"logisheets": {
|
|
190
|
+
"command": "npx",
|
|
191
|
+
"args": ["-y", "logisheets-mcp"],
|
|
192
|
+
"env": {"LOGISHEETS_MCP_TOOLS": "full"}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Mutating tools are marked with MCP's `readOnlyHint` / `destructiveHint`
|
|
199
|
+
annotations, so a host can gate them behind user approval.
|
|
200
|
+
|
|
201
|
+
## Blocks, briefly
|
|
202
|
+
|
|
203
|
+
A **block** is a named, structured region of a sheet — a table with a schema.
|
|
204
|
+
|
|
205
|
+
- The first field is the **row key**: the stable name of each record.
|
|
206
|
+
- Fields can carry a **value formula** (engine-computed, so the agent can't
|
|
207
|
+
write a stale number into it), a **validation** rule, or an **editability**
|
|
208
|
+
rule.
|
|
209
|
+
- Everything is addressed by name. Row and column indices never enter the
|
|
210
|
+
agent's reasoning.
|
|
211
|
+
|
|
212
|
+
Because blocks are created *by the agent as it works*, this needs no
|
|
213
|
+
pre-prepared file — you can point it at a blank workbook or at a spreadsheet
|
|
214
|
+
someone sent you.
|
|
215
|
+
|
|
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
|
+
## Getting the file back
|
|
256
|
+
|
|
257
|
+
`save_workbook` writes a real `.xlsx` and its result carries an MCP
|
|
258
|
+
**resource link** — a uri, media type and size — not the file. The workbook is
|
|
259
|
+
also listed as a resource (`workbook://current.xlsx`), so a host that wants the
|
|
260
|
+
bytes reads them with `resources/read` and hands the human a download.
|
|
261
|
+
|
|
262
|
+
That split is the point: a tool result goes into the model's context, where a
|
|
263
|
+
200 KB workbook would cost roughly 280 KB of text and teach the model nothing.
|
|
264
|
+
The link costs a line. `export_xlsx` still returns base64 for hosts that
|
|
265
|
+
implement no resources at all, but it is the fallback, not the mechanism.
|
|
266
|
+
|
|
267
|
+
Reads go through the same serialization lane as tool calls, so a host fetching
|
|
268
|
+
the file can never catch a half-applied transaction.
|
|
269
|
+
|
|
270
|
+
`open_workbook` and `save_workbook` read and write wherever the server process
|
|
271
|
+
can — normal for a local stdio server, and the same posture as the official
|
|
272
|
+
filesystem server. Both are marked as mutating so a host can prompt before they
|
|
273
|
+
run; if you need tighter limits, run the server as a user with only the access
|
|
274
|
+
you intend it to have.
|
|
275
|
+
|
|
276
|
+
## State model
|
|
277
|
+
|
|
278
|
+
One MCP session holds one active workbook, alive across tool calls — that
|
|
279
|
+
persistence is what makes it memory rather than a calculator. `open_workbook`
|
|
280
|
+
replaces it. Multiple named workbooks per session may come later.
|
|
281
|
+
|
|
282
|
+
## No network
|
|
283
|
+
|
|
284
|
+
The server opens no sockets and listens on no ports. "stdio transport" is
|
|
285
|
+
literal: your MCP host spawns this as a child process and they exchange
|
|
286
|
+
newline-delimited JSON-RPC over its stdin and stdout — the same pipes any
|
|
287
|
+
command-line program gets. The engine is WASM running in that same process,
|
|
288
|
+
so a formula is a function call, not a request.
|
|
289
|
+
|
|
290
|
+
Checked rather than asserted. After a full session — create a block, attach a
|
|
291
|
+
field rule, evaluate a formula, save an `.xlsx` — the process holds:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
fd types: {CHR: 2, DIR: 4, KQUEUE: 3, PIPE: 6, REG: 13}
|
|
295
|
+
network files (lsof -a -i): 0
|
|
296
|
+
unix sockets (lsof -a -U): 0
|
|
297
|
+
listening ports: 0
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Six pipes, no sockets. Nothing is uploaded, no telemetry is collected, and an
|
|
301
|
+
air-gapped machine is a supported way to run this. The only things it touches
|
|
302
|
+
outside its own memory are the files you name — see the filesystem note under
|
|
303
|
+
[Getting the file back](#getting-the-file-back).
|
|
304
|
+
|
|
305
|
+
That is the `logisheets-mcp` binary, which is what an MCP host runs. Using it
|
|
306
|
+
[as a library](#use-as-a-library) you can attach any transport you like,
|
|
307
|
+
including an HTTP one — but then the socket is yours, opened deliberately.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT. Part of the [LogiSheets](https://github.com/logisky/LogiSheets) project.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stdio entry point — what an MCP host (Claude Desktop, Cursor, Cline) spawns.
|
|
4
|
+
*
|
|
5
|
+
* stdout is the JSON-RPC channel and must carry nothing else, so every
|
|
6
|
+
* diagnostic in this process goes to stderr.
|
|
7
|
+
*/
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { createServer, SERVER_VERSION } from './server.js';
|
|
10
|
+
import { toolModeFromEnv } from './surface.js';
|
|
11
|
+
async function main() {
|
|
12
|
+
const argv = process.argv.slice(2);
|
|
13
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
14
|
+
process.stderr.write([
|
|
15
|
+
'logisheets-mcp — a real spreadsheet engine as an MCP server (stdio).',
|
|
16
|
+
'',
|
|
17
|
+
'Environment:',
|
|
18
|
+
' LOGISHEETS_MCP_TOOLS=core|full tool surface (default: core)',
|
|
19
|
+
'',
|
|
20
|
+
'Configure your MCP host to run this command; it speaks MCP on stdio,',
|
|
21
|
+
'not something you interact with directly in a terminal.',
|
|
22
|
+
'',
|
|
23
|
+
].join('\n'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
27
|
+
process.stderr.write(`${SERVER_VERSION}\n`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { server, session, tools } = createServer();
|
|
31
|
+
process.stderr.write(`logisheets-mcp ${SERVER_VERSION}: ${tools.size} tools (${toolModeFromEnv()} surface)\n`);
|
|
32
|
+
const shutdown = () => {
|
|
33
|
+
session.close();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
};
|
|
36
|
+
process.on('SIGINT', shutdown);
|
|
37
|
+
process.on('SIGTERM', shutdown);
|
|
38
|
+
await server.connect(new StdioServerTransport());
|
|
39
|
+
}
|
|
40
|
+
main().catch((err) => {
|
|
41
|
+
process.stderr.write(`logisheets-mcp failed to start: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logisheets-mcp — a real, Excel-compatible spreadsheet engine as an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Library entry point, for embedding the server in another host (an HTTP
|
|
5
|
+
* transport, an agent framework, a test). The stdio binary lives in ./cli.ts.
|
|
6
|
+
*/
|
|
7
|
+
export { createServer, INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, type CreatedServer, type CreateServerOptions, } from './server.js';
|
|
8
|
+
export { selectTools, toolModeFromEnv, type ToolMode, } from './surface.js';
|
|
9
|
+
export { WorkbookSession, type OpenResult, type SaveResult, type WorkbookClient, } from './session.js';
|
|
10
|
+
export { createLifecycleTools, type OpenWorkbookInput } from './lifecycle.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logisheets-mcp — a real, Excel-compatible spreadsheet engine as an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Library entry point, for embedding the server in another host (an HTTP
|
|
5
|
+
* transport, an agent framework, a test). The stdio binary lives in ./cli.ts.
|
|
6
|
+
*/
|
|
7
|
+
export { createServer, INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, } from './server.js';
|
|
8
|
+
export { selectTools, toolModeFromEnv, } from './surface.js';
|
|
9
|
+
export { WorkbookSession, } from './session.js';
|
|
10
|
+
export { createLifecycleTools } from './lifecycle.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workbook lifecycle tools — the one part of the surface logician doesn't
|
|
3
|
+
* already provide.
|
|
4
|
+
*
|
|
5
|
+
* logician's tools operate on an *open* workbook (it was written for the
|
|
6
|
+
* in-app assistant, where the host owns the file). A standalone MCP server has
|
|
7
|
+
* to own opening and handing back the file itself, so those three tools live
|
|
8
|
+
* here. They are ordinary logician `Tool`s that close over the session, which
|
|
9
|
+
* means the MCP adapter treats them identically to every engine tool.
|
|
10
|
+
*
|
|
11
|
+
* File paths are the primary interface: this server runs next to the agent, and
|
|
12
|
+
* a `.xlsx` round-tripped through base64 would cost tens of thousands of tokens
|
|
13
|
+
* in the model's context for no benefit. base64 stays available for hosts with
|
|
14
|
+
* no shared filesystem.
|
|
15
|
+
*/
|
|
16
|
+
import type { Tool } from 'logisheets-logician';
|
|
17
|
+
import type { WorkbookSession } from './session.js';
|
|
18
|
+
/**
|
|
19
|
+
* The active workbook, addressable as an MCP resource.
|
|
20
|
+
*
|
|
21
|
+
* A tool result goes into the model's context, so returning the file itself
|
|
22
|
+
* there costs roughly 1.4 KB of text per KB of workbook and teaches the model
|
|
23
|
+
* nothing. The protocol's answer is a resource: the tool returns a *link* — uri,
|
|
24
|
+
* mime type, size — and a host that wants the bytes fetches them with
|
|
25
|
+
* `resources/read`, outside the conversation. That is how the workbook gets
|
|
26
|
+
* handed back without the model paying for it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const WORKBOOK_URI = "workbook://current.xlsx";
|
|
29
|
+
/** The OOXML spreadsheet media type, as Excel and every host expect it. */
|
|
30
|
+
export declare const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
31
|
+
/**
|
|
32
|
+
* Tools whose result should carry a link to the workbook, so the host can offer
|
|
33
|
+
* the finished file to the human.
|
|
34
|
+
*/
|
|
35
|
+
export declare const TOOLS_YIELDING_WORKBOOK: ReadonlySet<string>;
|
|
36
|
+
export interface OpenWorkbookInput {
|
|
37
|
+
path?: string;
|
|
38
|
+
xlsx_base64?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
}
|
|
41
|
+
/** The lifecycle tools, bound to one session. */
|
|
42
|
+
export declare function createLifecycleTools(session: WorkbookSession): Tool[];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workbook lifecycle tools — the one part of the surface logician doesn't
|
|
3
|
+
* already provide.
|
|
4
|
+
*
|
|
5
|
+
* logician's tools operate on an *open* workbook (it was written for the
|
|
6
|
+
* in-app assistant, where the host owns the file). A standalone MCP server has
|
|
7
|
+
* to own opening and handing back the file itself, so those three tools live
|
|
8
|
+
* here. They are ordinary logician `Tool`s that close over the session, which
|
|
9
|
+
* means the MCP adapter treats them identically to every engine tool.
|
|
10
|
+
*
|
|
11
|
+
* File paths are the primary interface: this server runs next to the agent, and
|
|
12
|
+
* a `.xlsx` round-tripped through base64 would cost tens of thousands of tokens
|
|
13
|
+
* in the model's context for no benefit. base64 stays available for hosts with
|
|
14
|
+
* no shared filesystem.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The active workbook, addressable as an MCP resource.
|
|
18
|
+
*
|
|
19
|
+
* A tool result goes into the model's context, so returning the file itself
|
|
20
|
+
* there costs roughly 1.4 KB of text per KB of workbook and teaches the model
|
|
21
|
+
* nothing. The protocol's answer is a resource: the tool returns a *link* — uri,
|
|
22
|
+
* mime type, size — and a host that wants the bytes fetches them with
|
|
23
|
+
* `resources/read`, outside the conversation. That is how the workbook gets
|
|
24
|
+
* handed back without the model paying for it.
|
|
25
|
+
*/
|
|
26
|
+
export const WORKBOOK_URI = 'workbook://current.xlsx';
|
|
27
|
+
/** The OOXML spreadsheet media type, as Excel and every host expect it. */
|
|
28
|
+
export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
29
|
+
/**
|
|
30
|
+
* Tools whose result should carry a link to the workbook, so the host can offer
|
|
31
|
+
* the finished file to the human.
|
|
32
|
+
*/
|
|
33
|
+
export const TOOLS_YIELDING_WORKBOOK = new Set([
|
|
34
|
+
'save_workbook',
|
|
35
|
+
]);
|
|
36
|
+
function openWorkbook(session) {
|
|
37
|
+
return {
|
|
38
|
+
namespace: 'workbook',
|
|
39
|
+
name: 'open_workbook',
|
|
40
|
+
description: [
|
|
41
|
+
'Start the workbook you will work in. Call with no arguments for a fresh, empty one; pass `path` to load an existing .xlsx from disk and work on the human\'s real file.',
|
|
42
|
+
'',
|
|
43
|
+
'This replaces whatever workbook the session currently holds, discarding unsaved changes — so call it once at the start, not between steps. You do NOT have to call it at all: an empty workbook appears automatically the moment any other tool touches the session.',
|
|
44
|
+
'',
|
|
45
|
+
'Prefer `path` over `xlsx_base64`. `xlsx_base64` exists for hosts with no shared filesystem and costs enormous context for a file of any size.',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
mutates: true,
|
|
48
|
+
confirmation: 'always',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
properties: {
|
|
51
|
+
path: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Path to an existing .xlsx to load. Omit for an empty workbook.',
|
|
54
|
+
},
|
|
55
|
+
xlsx_base64: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'A .xlsx as base64, for when no shared filesystem exists. Use `path` when you can.',
|
|
58
|
+
},
|
|
59
|
+
name: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'File name to report to the engine when loading from base64.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
handler: async (input) => {
|
|
66
|
+
const result = await session.open({
|
|
67
|
+
path: input.path,
|
|
68
|
+
xlsxBase64: input.xlsx_base64,
|
|
69
|
+
name: input.name,
|
|
70
|
+
});
|
|
71
|
+
const where = result.path !== undefined
|
|
72
|
+
? ` from ${result.path}`
|
|
73
|
+
: result.source === 'bytes'
|
|
74
|
+
? ' from uploaded bytes'
|
|
75
|
+
: '';
|
|
76
|
+
return {
|
|
77
|
+
data: result,
|
|
78
|
+
display: `Opened ${result.source} workbook${where}: ${result.sheets.length} sheet(s) — ${result.sheets.join(', ')}`,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function saveWorkbook(session) {
|
|
84
|
+
return {
|
|
85
|
+
namespace: 'workbook',
|
|
86
|
+
name: 'save_workbook',
|
|
87
|
+
description: [
|
|
88
|
+
'Write the workbook to a real .xlsx file the human can open in Excel. This is how you hand your work back — do it when the task is done.',
|
|
89
|
+
'',
|
|
90
|
+
'Defaults to the path it was opened from, or last saved to; pass `path` to write somewhere else. Values, formulas and the block structure are all saved.',
|
|
91
|
+
'',
|
|
92
|
+
'IMPORTANT — if the human is going to work on this in Excel, pass `resolve_block_refs: true`. Formulas that read blocks are written as BLOCKREF/BLOCKREFS, which only LogiSheets understands: Excel shows the saved numbers but turns those cells into #NAME? the moment it recalculates. Resolving rewrites them as ordinary A1 references so Excel can recompute the model. Leave it off when the file is coming back here — the named form is readable and survives rows moving.',
|
|
93
|
+
'',
|
|
94
|
+
'The result carries a link to the workbook rather than its bytes, so the host can offer the human the file without any of it passing through your context.',
|
|
95
|
+
].join('\n'),
|
|
96
|
+
// Writes to the filesystem: not a workbook mutation, but very much an
|
|
97
|
+
// effect on the world, so hosts should gate it.
|
|
98
|
+
mutates: true,
|
|
99
|
+
confirmation: 'always',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
properties: {
|
|
102
|
+
path: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Destination .xlsx path. Defaults to the path the workbook was opened from.',
|
|
105
|
+
},
|
|
106
|
+
resolve_block_refs: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
default: false,
|
|
109
|
+
description: 'Rewrite BLOCKREF/BLOCKREFS as ordinary A1 references. Set this when the human will open the file in Excel; Excel has no BLOCKREF function and would show #NAME? on recalculation. One-way: a resolved file is an export, not a round trip.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
handler: async (input) => {
|
|
114
|
+
const result = await session.saveTo(input.path, {
|
|
115
|
+
resolveBlockRefs: input.resolve_block_refs === true,
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
data: result,
|
|
119
|
+
display: `Saved ${result.bytes} bytes to ${result.path}` +
|
|
120
|
+
(input.resolve_block_refs === true
|
|
121
|
+
? ' (block formulas resolved to A1 — Excel can recalculate it)'
|
|
122
|
+
: ''),
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function exportXlsx(session) {
|
|
128
|
+
return {
|
|
129
|
+
namespace: 'workbook',
|
|
130
|
+
name: 'export_xlsx',
|
|
131
|
+
description: [
|
|
132
|
+
'Return the workbook as base64-encoded .xlsx bytes.',
|
|
133
|
+
'',
|
|
134
|
+
'Last resort. The bytes land in your context and cost roughly 1.4 KB of text per KB of file. Prefer `save_workbook`, which writes a real file and hands the host a link to it — the host can give the human the workbook without any of it passing through you.',
|
|
135
|
+
].join('\n'),
|
|
136
|
+
mutates: false,
|
|
137
|
+
confirmation: 'never',
|
|
138
|
+
cost: 'expensive',
|
|
139
|
+
inputSchema: { properties: {} },
|
|
140
|
+
handler: async () => {
|
|
141
|
+
const result = session.exportBase64();
|
|
142
|
+
return {
|
|
143
|
+
data: result,
|
|
144
|
+
display: `Exported ${result.bytes} bytes as base64`,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The lifecycle tools, bound to one session. */
|
|
150
|
+
export function createLifecycleTools(session) {
|
|
151
|
+
return [
|
|
152
|
+
openWorkbook(session),
|
|
153
|
+
saveWorkbook(session),
|
|
154
|
+
exportXlsx(session),
|
|
155
|
+
];
|
|
156
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP shell: logician tools in, MCP protocol out.
|
|
3
|
+
*
|
|
4
|
+
* We use the SDK's low-level `Server` rather than `McpServer` on purpose.
|
|
5
|
+
* `McpServer.registerTool` wants Zod schemas, while logician tools already
|
|
6
|
+
* carry hand-written JSON Schema — which is what MCP puts on the wire anyway.
|
|
7
|
+
* Going low-level passes those straight through instead of round-tripping them
|
|
8
|
+
* through a Zod translation layer.
|
|
9
|
+
*/
|
|
10
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
+
import type { Tool } from 'logisheets-logician';
|
|
12
|
+
import { WorkbookSession } from './session.js';
|
|
13
|
+
import { type ToolMode } from './surface.js';
|
|
14
|
+
export declare const SERVER_NAME = "logisheets";
|
|
15
|
+
export declare const SERVER_VERSION = "0.1.0";
|
|
16
|
+
/**
|
|
17
|
+
* How the agent should approach this server. Sent as MCP `instructions`, so a
|
|
18
|
+
* host can put it in front of the model before it starts guessing.
|
|
19
|
+
*/
|
|
20
|
+
export declare const INSTRUCTIONS: string;
|
|
21
|
+
export interface CreateServerOptions {
|
|
22
|
+
/** Reuse an existing session (tests, embedding). Defaults to a fresh one. */
|
|
23
|
+
session?: WorkbookSession;
|
|
24
|
+
/** Override the tool surface; defaults to `LOGISHEETS_MCP_TOOLS` or `core`. */
|
|
25
|
+
mode?: ToolMode;
|
|
26
|
+
/**
|
|
27
|
+
* Where tool progress lines go. Defaults to stderr — on the stdio
|
|
28
|
+
* transport, stdout carries JSON-RPC frames and writing anything else
|
|
29
|
+
* there corrupts the stream.
|
|
30
|
+
*/
|
|
31
|
+
log?: (msg: string) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface CreatedServer {
|
|
34
|
+
server: Server;
|
|
35
|
+
session: WorkbookSession;
|
|
36
|
+
/** The tools exposed, by MCP name. */
|
|
37
|
+
tools: Map<string, Tool>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the server. Nothing is started — hand the result to a transport.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createServer(opts?: CreateServerOptions): CreatedServer;
|