This page is for agent builders who want workbook formulas behind a Model Context Protocol tool surface. The useful boundary is small: list the tools, call one tool, return exact cell readback, and include enough structured output for the agent to verify the edit.
@bilig/headless owns the workbook behavior. MCP should stay as the transport
and discovery layer around ordinary Node functions.
Run the dependency-free example from a clean checkout:
git clone https://github.com/proompteng/bilig.git
cd bilig/examples/headless-workpaper
npm install
npm run agent:mcp-tools
For a local stdio transport, pipe newline-delimited JSON-RPC requests into the stdio entrypoint:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' |
npm run --silent agent:mcp-stdio
Use the maintained transcript smoke when reviewing the server from an MCP client, directory submission, or HN-style launch thread:
cd examples/headless-workpaper
npm install
NODE_NO_WARNINGS=1 npm run --silent agent:mcp-transcript
The script starts the stdio server, sends initialize, tools/list, and
tools/call, parses the JSON-RPC responses, asserts the formula readback, and
prints a compact transcript summary. The important response is the tools/call
result. A passing run returns structured content like this:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"structuredContent": {
"editedCell": "Inputs!B3",
"before": {
"expectedCustomers": 5,
"expectedArr": 60000,
"expansionArr": 66000,
"targetGap": -34000
},
"after": {
"expectedCustomers": 8,
"expectedArr": 96000,
"expansionArr": 105600,
"targetGap": 5600
},
"restored": {
"expectedCustomers": 8,
"expectedArr": 96000,
"expansionArr": 105600,
"targetGap": 5600
},
"formulaContracts": {
"expectedCustomers": "=Inputs!B2*Inputs!B3",
"expectedArr": "=B2*Inputs!B4",
"expansionArr": "=B3*Inputs!B5",
"targetGap": "=B4-100000"
},
"checks": {
"previousValue": 0.25,
"newValue": 0.4,
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true,
"serializedBytes": 1163
}
},
"isError": false
}
}
That single response proves the tool changed one input cell, recalculated dependent formulas, preserved the formulas through WorkPaper JSON serialization, restored the document, and returned machine-checkable readback.
If you want the raw newline-delimited JSON-RPC request stream instead of the maintained transcript wrapper, use:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"set_workpaper_input_cell","arguments":{"sheetName":"Inputs","address":"B3","value":0.4}}}' |
NODE_NO_WARNINGS=1 npm run --silent agent:mcp-stdio
The npm package exposes the demo server as bilig-workpaper-mcp by default:
npm exec --package @bilig/headless -- bilig-workpaper-mcp
For a real agent workflow, point the same binary at a persisted WorkPaper JSON document:
npm exec --package @bilig/headless -- bilig-workpaper-mcp --workpaper ./pricing.workpaper.json --writable
File-backed mode loads ./pricing.workpaper.json, exposes list_sheets,
read_range, read_cell, set_cell_contents, get_cell_display_value,
export_workpaper_document, and validate_formula, then writes the updated
WorkPaper JSON back to the same file after set_cell_contents when --writable
is present. Omit --writable for read-only inspection.
The package carries mcpName: io.github.proompteng/bilig-workpaper and a
matching server.json. It is published in the official MCP Registry as
io.github.proompteng/bilig-workpaper:
https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.proompteng%2Fbilig-workpaper.
If you already know which client you want to use, start with the MCP client setup guide for Claude, Cursor, VS Code, and Codex config snippets.
If you are checking a directory listing or preparing one, use the MCP spreadsheet server directory status page for the canonical npm command, official Registry proof, Glama listing, and pending directory-review status.
Before submitting the server to an MCP registry, verify this repo-specific readiness checklist:
packages/headless/server.json exists and describes the packaged stdio
server.packages/headless/package.json exposes bilig-workpaper-mcp in bin.packages/headless/package.json includes
mcpName: io.github.proompteng/bilig-workpaper.pnpm publish:runtime:check passes against the runtime packages.pnpm workpaper:smoke:external passes against packed local runtime packages.Passing the checklist means the repository metadata and smoke checks are ready for registry submission; it does not mean the package has already been published.
If your agent loop already uses the Vercel AI SDK, keep the MCP client thin and let the WorkPaper server own the spreadsheet reads and writes:
import { createMCPClient } from '@ai-sdk/mcp'
import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio'
import { generateText } from 'ai'
const client = await createMCPClient({
transport: new Experimental_StdioMCPTransport({
command: 'npm',
args: ['exec', '--package', '@bilig/headless', '--', 'bilig-workpaper-mcp'],
}),
})
try {
const tools = await client.tools()
const { text } = await generateText({
model: 'your-model',
tools,
prompt: [
'Read the WorkPaper summary with read_workpaper_summary for Summary!A1:B5.',
'Then set Inputs!B3 to 0.4 with set_workpaper_input_cell.',
'Return editedCell plus the before and after expectedArr values.',
].join('\n'),
})
console.log(text)
} finally {
await client.close()
}
The server command is bilig-workpaper-mcp; the npm exec --package
@bilig/headless -- bilig-workpaper-mcp wrapper only resolves the published npm
package for a clean checkout. The stdio transport receives npm as the command
and the rest as args, so shell parsing does not sit between the AI SDK client
and the MCP server. The two tool calls prove the useful workflow: read a
formula-backed summary, set one input cell, and return computed before/after
readback.
Verify the docs links and discovery metadata after editing this page:
pnpm docs:discovery:check
The script implements two JSON-RPC methods shaped around the MCP tool model:
tools/list returns read_workpaper_summary and
set_workpaper_input_cell with JSON Schema inputs and MCP tool annotations.tools/call invokes the requested WorkPaper tool and returns text content
plus structured formula readback.The packaged binary has two tool sets:
read_workpaper_summary and set_workpaper_input_celllist_sheets, read_range, read_cell,
set_cell_contents, get_cell_display_value, export_workpaper_document,
and validate_formulaThe annotations are explicit for directory reviewers and cautious MCP clients:
read_workpaper_summary is read-only, idempotent, and closed-world.
set_workpaper_input_cell mutates the local WorkPaper state, is idempotent for
the same cell/value arguments, and is closed-world rather than a network or
filesystem tool.
In file-backed mode, set_cell_contents is annotated as destructive only when
the server starts with --writable.
| Symptom | What to check |
|---|---|
Parse error response |
Make sure each stdin line is valid JSON before it reaches the server. |
| No response appears | End each JSON-RPC message with a newline; the server waits for newline-delimited input. |
| Notification has no output | notifications/initialized is intentionally one-way and does not produce a JSON-RPC response. |
Invalid params or tool error |
Check that tools/call includes a supported name and the required arguments for that tool. |
The example deliberately avoids an MCP SDK dependency so the workbook contract is visible. Put the same handlers behind stdio, HTTP, or your MCP SDK adapter when you wire it into a production agent host.
The write tool edits Inputs!B3, recalculates dependent formulas, serializes
the WorkPaper document, restores it, and checks that formulas and computed
values survived the round trip:
{
"editedCell": "Inputs!B3",
"before": {
"expectedCustomers": 5,
"expectedArr": 60000,
"expansionArr": 66000,
"targetGap": -34000
},
"after": {
"expectedCustomers": 8,
"expectedArr": 96000,
"expansionArr": 105600,
"targetGap": 5600
},
"checks": {
"previousValue": 0.25,
"newValue": 0.4,
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
That is the part spreadsheet agents need. A tool that only says “updated” is not enough. Return the edited address, previous value, new value, before/after computed values, formula contracts, and persistence proof.
Expose only the minimum useful surface first:
read_workpaper_summary reads a bounded range and returns computed values
plus serialized cell contents.set_workpaper_input_cell validates the sheet and A1 address before a
write, then returns formula readback and persistence checks.The official MCP specification describes tool discovery through tools/list,
tool invocation through tools/call, input schemas, and tool annotations:
https://modelcontextprotocol.io/specification/2025-06-18/server/tools.
examples/headless-workpaper/mcp-tool-server.tsexamples/headless-workpaper/mcp-stdio-server.tsio.github.proompteng/bilig-workpaperexamples/headless-workpaper/README.md#mcp-tool-server-shapedocs/agent-workpaper-tool-calling-recipe.mddocs/vercel-ai-sdk-langchain-spreadsheet-tool.mdUse the MCP spreadsheet tool server discussion for adapter feedback. The open questions are deliberately concrete: stdio, HTTP/SSE, or SDK adapter next; which spreadsheet workflow should be proven next; and which structured fields every write tool should return.
Use this pattern when an agent needs to edit a forecast, pricing workbook, quote approval rule, budget check, or service-side spreadsheet model and prove the formulas reacted. Keep the MCP layer thin, keep the workbook logic testable, and make every write return structured verification.
Start with the adapter command above. If it saves you a spreadsheet-tooling spike, star the repository so the next person searching for MCP spreadsheet tools can find it: https://github.com/proompteng/bilig/stargazers.
If it almost matches but a gap blocks adoption, use the adoption blocker form: https://github.com/proompteng/bilig/discussions/new?category=general.