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 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
@@ -0,0 +1,8 @@
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
+ export {};
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
+ });
@@ -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
+ }
@@ -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;