cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Docs, skill and prompts: one 012 mcp for every workbook

Dami committed 17c7f50
The MCP server page says which files a server opens, how
012 agent --install-mcp adds it to Claude Code, Codex and Claude
Desktop, and how the view works in MCP Apps and Apps SDK hosts. The
skill tells agents to install it with --install-mcp, never for one
file. Prompts take the workbook's path.
commit
17c7f50e27294767e6f0890e1c8ba41e203ba2cd
parent
1ef0029b42fe1663a6fa39411d58b438dd208788
author
Dami <dami.etoile@gmail.com>
kind
docs risk score 1.9 of 3, Jev's estimate
9 files changed, +190 -87
MROADMAP.md4+-
Mcmd/012/skill/SKILL.md24++++-
Mdocs/agents/README.md2+-
Mdocs/agents/mcp.md162+++++++++++++++++++-----------
Mdocs/contributing/testing.md16+++
Mdocs/reference/command-line.md5+-
Mdocs/reference/json.md29++++-
Minternal/mcp/prompts.go33++++--
Minternal/mcp/server_test.go2+-
M ROADMAP.md +2 -2
M cmd/012/skill/SKILL.md +21 -3
M docs/agents/README.md +1 -1
M docs/agents/mcp.md +100 -62
@@ -5,57 +5,77 @@ sidebar_position: 2
55 66 # MCP server77 8-`012 mcp book.012` is a [Model Context Protocol](https://modelcontextprotocol.io)9-server on one workbook, over standard input and output, so any MCP host10-(Claude Code, Claude Desktop, editors) can read and change it. It is the11-same code as [the commands](README.md#with-a-shell): reads are12-`012 describe` and `012 get`, and every write is typed through the13-checks `012 set` makes, as one change, saved atomically at14-once.8+`012 mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)9+server over standard input and output, so any MCP host (Claude Code,10+Codex, Claude Desktop, ChatGPT, editors) can read and change the11+workbooks in the folders open to it. One server works on every12+workbook: each tool takes the workbook's `path`. It is the same code as13+[the commands](README.md#with-a-shell): reads are `012 describe` and14+`012 get`, and every write is typed through the checks `012 set` makes,15+as one change, saved atomically at once.1516 1617 ```mermaid1718 flowchart LR18- host[MCP host] -- "stdio: tools, resources, prompts" --> server["012 mcp book.012"]19- server -- "open, read" --> file[(book.012)]19+ host[MCP host] -- "stdio: tools with a path, resources, prompts" --> server["012 mcp"]20+ server -- "open, read" --> file[("budget.012, q3/plan.012, data.csv")]2021 server -- "one change, Batch, atomic save" --> file21- person["012 book.012, 012 set, git"] --> file22+ person["012 budget.012, 012 set, git"] --> file2223 ```2324 24-The file is opened afresh for every call, so what the screen, a script25-or git saved meanwhile is what the next call sees. A file that doesn't26-exist is an empty workbook, saved on the first change.25+A file is opened afresh for every call, so what the screen, a script26+or git saved meanwhile is what the next call sees.2727 2828 ## Adding it to a host2929 30-Claude Code, for one workbook (`--scope project` writes `.mcp.json`31-to commit with the project; the default keeps it to you):30+`012 agent --install-mcp` writes the entry that starts this 012, by its31+absolute path and with no workbook, into the host's configuration:3232 3333 ```sh34-claude mcp add budget -- 012 mcp /path/to/budget.01235-claude mcp add --scope project budget -- 012 mcp budget.01234+012 agent --install-mcp claude-code # Claude Code: ~/.claude.json, as claude mcp add --scope user does35+012 agent --install-mcp codex # Codex: [mcp_servers.012] in ~/.codex/config.toml36+012 agent --install-mcp claude-desktop # Claude Desktop: claude_desktop_config.json37+012 agent --install-mcp codex --print # show the entry, write nothing38+012 agent --install-mcp claude-desktop --root ~/Documents # open these folders to it3639 ```3740 38-Claude Desktop, in `claude_desktop_config.json` (Settings > Developer >39-Edit Config), with the full paths, since Desktop doesn't start in your40-folder:41-42-```json43-{44- "mcpServers": {45- "budget": {46- "command": "/usr/local/bin/012",47- "args": ["mcp", "/Users/me/Documents/budget.012"]48- }49- }50-}51-```52-53-Other hosts take the same command and arguments.41+The file is copied beside itself (`config.toml.bak-20260929-150405`)42+before it changes, the host's other servers and settings stay as they43+were, and running it again when the entry is there changes nothing.44+It names other entries that start 012, such as one made earlier for a45+single workbook, which the new entry replaces. Restart the host to46+start the server; install again after moving 012.47+48+Other hosts take the command and arguments `--print` shows. A file49+named after `mcp` (`012 mcp budget.012`) is the workbook of tools50+called without a path, open wherever it is.51+52+## Which files it opens53+54+A `path` is confined to the folders the host shares as its roots55+(Claude Code shares the project's), or when it shares none, to the56+folders `--root` names (any number of them), or else the working57+directory, or the home folder when that's the file system's root, as58+where Claude Desktop starts servers. A relative path is looked for in59+each folder in turn; an absolute one must be inside one. Hidden files60+and folders and links that lead outside are refused, as61+[`012 serve`](../terminal/ssh.md#security-model) refuses them. Roots are deprecated in62+the protocol's 2026-07-28 version, whose clients the server can't ask,63+so they get `--root`'s folders.64+65+`describe` without a path lists the workbooks in those folders: `.012`66+files, and the files 012 [imports](../files/README.md#import-and-download) (CSV, XLSX,67+JSON, SQLite and the rest), four folders deep, without hidden folders68+or `node_modules`. Imported files are read as the app opens them, and69+are read only: `create_workbook` with `from` makes a `.012` workbook70+of one, to change. A path that doesn't exist is an error naming71+`create_workbook`, which makes a new workbook and never replaces a file.5472 5573 ## Flags5674 5775 | Flag | Does |5876 |---|---|77+| `file.012` | The workbook of tools called without a path |78+| `--root dir` | A folder workbooks may be in, when the host shares no roots; give it again for more |5979 | `--read-only` | Offers only the tools that read |6080 | `--force` | Lets writes change [protected ranges](../sheets/notes-protection.md#protected-sheets-and-ranges), as `012 set --force` |6181 | `--notebooks` | Offers `run_notebook_cell`, which runs a [notebook](../nushell/notebooks.md) cell with nu, if the workbook was saved on this computer |
@@ -69,18 +89,21 @@ notebook cells from running even with `--notebooks`.
6989 7090 ## Tools7191 72-References are written as in formulas (`B7`, `Q3!A1:C9`,73-`'Q3 plan'!A:A`, a named range, a table, `Sales[Amount]`, a sheet's74-name), and inputs as a person types them, in en-US form. Every write75-takes `dry_run`, which returns the change without making it.92+Every tool but `describe` takes `path`, the workbook, required unless93+the server was started with a file. References are written as in94+formulas (`B7`, `Q3!A1:C9`, `'Q3 plan'!A:A`, a named range, a table,95+`Sales[Amount]`, a sheet's name), and inputs as a person types them, in96+en-US form. Every write takes `dry_run`, which returns the change97+without making it.7698 7799 | Tool | Does |78100 |---|---|79-| `describe` | The workbook's sheets, used ranges, guessed header rows and column names, tables, outputs, charts, pivots, notebook cells and names, as [`012 describe`](../reference/json.md#describe) |101+| `describe` | Without a path, the workbooks open to the server; with one, its sheets, used ranges, guessed header rows and column names, tables, outputs, charts, pivots, notebook cells and names, as [`012 describe`](../reference/json.md#describe) |80102 | `read_range` | A range's values as rows, its formulas by cell, and with `text` each cell as shown |81103 | `evaluate` | A formula's value, computed in the workbook (at `at`, or below the data) without writing it: its text, why it's an error, an array's spill |82104 | `find` | Cells by what they show or, with `in_formulas`, their formulas' text, as Edit > Find |83105 | `list_errors` | The formulas showing errors after recalculating, as `012 recalc` lists them, without saving |106+| `create_workbook` | A new `.012` workbook at `path`, empty or made `from` a file 012 imports or another workbook; it never replaces a file |84107 | `write_cells` | Entries typed into cells as one change, as `012 set` |85108 | `apply_operations` | Several operations as one change: `set`, `clear`, `insert_rows`, `delete_rows`, `insert_columns`, `delete_columns`, `add_sheet`, `rename_sheet`, `delete_sheet`, `define_name`, `sort` |86109 | `sort` | A range's rows sorted by columns, named by letter or header |
@@ -96,40 +119,55 @@ with nothing changed. Every result's schema is in
96119 97120 ## Views in the chat98121 99-Hosts that support [MCP Apps](https://github.com/modelcontextprotocol/ext-apps)100-(the `io.modelcontextprotocol/ui` extension) draw what `read_range`101-read and the chart `create_chart` made beside the result, as 012 draws102-them: the [web page](../files/README.md#web-pages) export's grid, with103-its fonts, colors, styles and charts as SVG, in the host's light or104-dark theme. Clicking a cell, or the arrow keys, puts the pointer on it105-and shows what was typed in it.106-107-Both tools name the view's resource, `ui://012/view`108-(`text/html;profile=mcp-app`), in their `_meta`; the host loads it into109-a sandboxed frame, and the result's `_meta` carries what to draw, at110-most 500 rows of a range. Hosts without the extension get the same111-results without it, and the page asks for nothing from the network.122+Hosts that draw tool results as pages show what `read_range` read and123+the chart `create_chart` made beside the result, as 012 draws them: the124+[web page](../files/README.md#web-pages) export's grid with its column125+and row headers, fonts, number formats, styles and charts as SVG, in126+the host's light or dark theme. Clicking a cell, or the arrow keys,127+puts the pointer on it and shows what was typed in it in the formula128+bar. The grid scrolls under its headers in at most 480 pixels inline129+(less when the host has less room, the whole frame full screen), and a130+smaller range takes only its own height.131+132+Two kinds of hosts do this, and the view works in both:133+134+| Host | Finds the page by | Its page | The page reads |135+|---|---|---|---|136+| [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) (Claude, and the extension's other hosts) | the tool's `_meta.ui.resourceUri` | `ui://012/view`, `text/html;profile=mcp-app` | the `ui/initialize` handshake's theme and room, then `ui/notifications/tool-result` |137+| The [OpenAI Apps SDK](https://developers.openai.com/apps-sdk) (ChatGPT, Codex) | the tool's `_meta["openai/outputTemplate"]` | `ui://012/view.skybridge`, `text/html+skybridge` | `window.openai`: `toolOutput`, `theme`, `maxHeight`, `displayMode`, and `openai:set_globals` events |138+139+What the page draws is in the result's `structuredContent.view`, for140+every client, whether or not it declared the extension: the workbook's141+name, where the range or chart is, and its HTML, at most 200 rows of142+what `read_range` returned. The result's text, which models read, is143+the same result without the view; hosts of the Apps SDK also show the144+model `structuredContent`, view included. The page asks for nothing145+from the network and calls no tools.112146 113147 ## Resources114148 115149 Hosts that let people attach data to a conversation list these; the116-scheme is `o12` since a URI's scheme starts with a letter, and a sheet's117-name is percent-encoded:150+scheme is `o12` since a URI's scheme starts with a letter. The151+workbook is named as tools name it (relative to the first folder open152+to the server, or absolute, `o12:///Users/me/budget.012/...`), each153+part of its path and a sheet's name percent-encoded:118154 119155 | URI | Is |120156 |---|---|121-| `o12://book/Sheet1!A1:D40` | A range, as `read_range` returns it; each sheet's used range is listed |122-| `o12://book/table/Sales` | A table, header row included; each table is listed |123-| `o12://book/notebook/Notes/2` | A notebook cell's source, and its output as NUON or why it failed; each code cell is listed |157+| `o12://budget.012/Sheet1!A1:D40` | A range, as `read_range` returns it; each sheet's used range is listed |158+| `o12://budget.012/table/Sales` | A table, header row included; each table is listed |159+| `o12://budget.012/notebook/Notes/2` | A notebook cell's source, and its output as NUON or why it failed; each code cell is listed |124160 125-The list follows the workbook: a change that adds a sheet or table126-updates it, and hosts that listen are told.161+The list holds the five workbooks the tools used last (and the file162+named on the command line) and follows them: a change that adds a sheet163+or table updates it, and hosts that listen are told. The templates164+reach any range of any workbook open to the server.127165 128166 ## Prompts129167 130168 | Prompt | Asks |131169 |---|---|132-| `summarize_workbook` | What each sheet holds and what its numbers say, without changing anything |133-| `fix_errors` | The formulas showing errors explained, fixes checked with `evaluate` and previewed before they're made |134-| `add_column` | A column of formulas computing something beside a range (`range`, `column`) |135-| `chart` | The chart that best answers a question about a range (`range`, `question`) |170+| `summarize_workbook` | What each sheet of a workbook (`path`) holds and what its numbers say, without changing anything |171+| `fix_errors` | The formulas showing errors in a workbook (`path`) explained, fixes checked with `evaluate` and previewed before they're made |172+| `add_column` | A column of formulas computing something beside a range (`path`, `range`, `column`) |173+| `chart` | The chart that best answers a question about a range (`path`, `range`, `question`) |
M docs/contributing/testing.md +16 -0
M docs/reference/command-line.md +3 -2
@@ -29,12 +29,13 @@ The flags are the options' flags, listed in [Configuration](config.md#options).
2929 | `012 recalc file.012` | Recalculates, saves and lists the cells showing errors: [Scripts](../files/scripts.md#recalc) |3030 | `012 export file.012 out [ref]` | Writes the workbook, a sheet or a range in another format: [Scripts](../files/scripts.md#export) |3131 | `012 describe file.012` | Lists the sheets and what's on them: [Agents](../agents/README.md#with-a-shell) |32-| `012 mcp file.012` | Serves the workbook to MCP hosts over standard input and output: [MCP server](../agents/mcp.md) |32+| `012 mcp [file.012]` | Serves the workbooks in the folders open to it to MCP hosts over standard input and output: [MCP server](../agents/mcp.md) |3333 | `012 diff a.012 b.012` | Lists what changed, cell by cell: [Diff and merge in git](../files/git.md#012-diff) |3434 | `012 diff --textconv file.012` | Writes a workbook as lines, for git's textconv: [Diff and merge in git](../files/git.md#git) |3535 | `012 merge-driver base ours theirs [path]` | Merges theirs into ours, for git: [Diff and merge in git](../files/git.md#merging) |3636 | `012 nu --module`, `012 nu --install-module [--force] [path]` | Prints or installs the nushell module: [Nushell](../nushell/README.md#install-the-sheet-command) |3737 | `012 agent --skill`, `012 agent --install-skill [--force] [dir]` | Prints or installs the Claude Code skill: [Agents](../agents/README.md#the-claude-code-skill) |38+| `012 agent --install-mcp claude-code\|claude-desktop\|codex [--root dir]... [--print]` | Adds `012 mcp` to a host's configuration, or prints the entry: [MCP server](../agents/mcp.md#adding-it-to-a-host) |3839 | `012 config [path\|edit\|default\|themes\|set-key\|delete-key]` | The settings: [Configuration](config.md#commands) |3940 | `012 version [--format text\|json\|nuon]` | Prints the version |4041
@@ -57,7 +58,7 @@ flag.
5758 | `--chart n\|title` | `export` | One chart of the sheet as a web page, by its number or title |5859 | `--notebooks` | `get`, `recalc`, `export`, `describe` | Runs the notebooks' cells first, when the file is trusted here |5960 | `--trust` | with `--notebooks` | Runs the cells of a file saved on another computer |60-| `--read-only`, `--force`, `--notebooks`, `--jev` | `mcp` | The tools offered and what they may do: [MCP server](../agents/mcp.md#flags) |61+| `--root dir`, `--read-only`, `--force`, `--notebooks`, `--jev` | `mcp` | The folders open to it, the tools offered and what they may do: [MCP server](../agents/mcp.md#flags) |6162 | `--jev` | `get`, `recalc`, `export`, `describe` | Asks JEV for the JEV functions' answers first |6263 | `--format text\|json\|nuon` | `diff` | The form of the changes; text by default |6364 | `--color auto\|always\|never` | `diff` | Colors the text; auto colors on a terminal or git's pager unless `NO_COLOR` is set |
M docs/reference/json.md +23 -6
@@ -129,9 +129,24 @@ are as without it.
129129 ## MCP tools130130 131131 The [MCP server](../agents/mcp.md)'s tools return these, as structured132-content with the same JSON as text beside it. `describe` returns133-[describe's](#describe) schema, `list_errors` recalc's `errors` and134-`circular`.132+content with the same JSON as text beside it (the text without the133+`view`). `describe` with a path returns `path` and134+[describe's](#describe) schema, and so does `create_workbook`, of the135+workbook it made; without a path, `describe` returns the folders open136+to the server and the workbooks in them, `more` set when there were137+more than it lists:138+139+```json140+{141+ "roots": ["/Users/me/Documents/budgets"],142+ "workbooks": [143+ {"path": "2026.012", "format": "012", "size": 18422, "modified": "2026-09-29T14:02:11Z"},144+ {"path": "bank/september.csv", "format": "CSV", "size": 5120, "modified": "2026-09-28T09:30:00Z"}145+ ]146+}147+```148+149+`list_errors` returns recalc's `errors` and `circular`.135150 136151 `read_range`, and the range resources:137152
@@ -148,13 +163,15 @@ content with the same JSON as text beside it. `describe` returns
148163 `values` are typed as [`get`](#get) types one cell (blank is `null`, an149164 error its text); `text` comes only when asked for; `truncated` is set150165 when the range held more cells than `max_cells`, and `rows` says how151-many came back.166+many came back. The tool adds `view`, what167+[the view](../agents/mcp.md#views-in-the-chat) draws: `title` (the168+workbook's name), `where` and `html`; the range resources leave it out.152169 153170 Writes (`write_cells`, `apply_operations`, `sort`, `filter`) return154171 `{"saved", "changes", "warnings"}` as [`set`](#set) does, `saved` false155172 for a dry run or a change that changed nothing; `create_chart` adds156-`"chart": {"sheet", "number", "title"}` and `create_pivot` `"sheet"`,157-the pivot's.173+`"chart": {"sheet", "number", "title"}` and its `view` (with `name`,174+the chart's data), and `create_pivot` `"sheet"`, the pivot's.158175 159176 | Tool | Returns |160177 |---|---|
M internal/mcp/prompts.go +23 -10
@@ -15,44 +15,57 @@ var prompts = []struct {
1515 text string // with {arg} for each argument1616 }{{1717 prompt: &sdk.Prompt{Name: "summarize_workbook", Title: "Summarize the workbook",18- Description: "What each sheet holds and what its numbers say"},19- text: "Call describe, then read the main ranges it points at with read_range (a few hundred rows at most). " +18+ Description: "What each sheet holds and what its numbers say", Arguments: []*sdk.PromptArgument{pathArg}},19+ text: "Call describe on {path}, then read the main ranges it points at with read_range (a few hundred rows at most). " +2020 "Summarize what each sheet holds, how the sheets relate (formulas between them, tables, pivots, charts), and what the numbers say. Don't change anything.",2121 }, {2222 prompt: &sdk.Prompt{Name: "fix_errors", Title: "Fix formula errors",23- Description: "Find the formulas showing errors, explain them and fix them"},24- text: "Call list_errors. For each formula showing an error, read the cells it reads, and explain the cause. " +23+ Description: "Find the formulas showing errors, explain them and fix them", Arguments: []*sdk.PromptArgument{pathArg}},24+ text: "Call list_errors on {path}. For each formula showing an error, read the cells it reads, and explain the cause. " +2525 "Propose a fix, check it with evaluate at the same cell, and show it with write_cells and dry_run. Make the fixes once they're agreed.",2626 }, {2727 prompt: &sdk.Prompt{Name: "add_column", Title: "Add a computed column",2828 Description: "A new column of formulas beside a table of data",29- Arguments: []*sdk.PromptArgument{29+ Arguments: []*sdk.PromptArgument{pathArg,3030 {Name: "range", Description: "the data, e.g. Sales!A1:F99, or a table's name", Required: true},3131 {Name: "column", Description: "what the new column computes, e.g. profit as revenue minus cost", Required: true},3232 }},33- text: "Add a column to {range} that computes {column}. Call describe and read_range to find the header row and the columns involved. " +33+ text: "Add a column to {range} in {path} that computes {column}. Call describe and read_range to find the header row and the columns involved. " +3434 "Write the header, then the formula for the first data row: check it with evaluate at that cell, then write every row's formula with write_cells (dry_run first), relative references moving with the row. " +3535 "Follow the columns' number formats. Finish with list_errors.",3636 }, {3737 prompt: &sdk.Prompt{Name: "chart", Title: "Chart some data",3838 Description: "A chart that answers a question about a range",39- Arguments: []*sdk.PromptArgument{39+ Arguments: []*sdk.PromptArgument{pathArg,4040 {Name: "range", Description: "the data, e.g. Sales!A1:C13", Required: true},4141 {Name: "question", Description: "what the chart should show, e.g. how sales grew by month"},4242 }},43- text: "Read {range} with read_range and pick the chart that best shows {question}: line or area for change over time, column or bar to compare, pie for shares of a whole, scatter for two measures. " +43+ text: "Read {range} in {path} with read_range and pick the chart that best shows {question}: line or area for change over time, column or bar to compare, pie for shares of a whole, scatter for two measures. " +4444 "If the data needs summarizing first, use create_pivot and chart its result. Create it with create_chart, with a title that says what it shows.",4545 }}4646 47+// pathArg is every prompt's workbook; without it the model finds one.48+var pathArg = &sdk.PromptArgument{Name: "path", Description: "the workbook, e.g. budget.012; describe without a path lists them"}49+50+// unset are what a prompt says for the arguments not given.51+var unset = map[string]string{52+ "{question}": "the data's main point",53+ "{path}": "the workbook (describe without a path lists them)",54+}55+4756 func (s *Server) addPrompts() {4857 for _, p := range prompts {4958 text := p.text5059 s.AddPrompt(p.prompt, func(_ context.Context, req *sdk.GetPromptRequest) (*sdk.GetPromptResult, error) {5160 msg := text5261 for k, v := range req.Params.Arguments {53- msg = strings.ReplaceAll(msg, "{"+k+"}", v)62+ if v != "" {63+ msg = strings.ReplaceAll(msg, "{"+k+"}", v)64+ }65+ }66+ for k, v := range unset {67+ msg = strings.ReplaceAll(msg, k, v)5468 }55- msg = strings.ReplaceAll(msg, "{question}", "the data's main point")5669 return &sdk.GetPromptResult{Description: req.Params.Name,5770 Messages: []*sdk.PromptMessage{{Role: "user", Content: &sdk.TextContent{Text: msg}}}}, nil5871 })
M internal/mcp/server_test.go +1 -1