Use this path when an OpenAI Agents SDK app needs a workbook tool it can call from Node without opening Excel, LibreOffice, Google Sheets, or a screenshot UI. There are two maintained integration shapes:
MCPServerStdio.The direct function-tool path gives the agent two ordinary function tools:
read_workpaper_summary reads computed WorkPaper values and serialized cells.set_workpaper_input_cell writes one validated input cell and returns
before/after readback, formula persistence checks, and restored JSON proof.The maintained smoke script is provider-free by default. It imports
Agent, tool(), RunContext, and invokeFunctionTool() from
@openai/agents, creates a real SDK agent and function tools, then invokes the
tools locally so the read/write contract can run in CI without an API key:
pnpm --dir examples/headless-workpaper run agent:openai-agents-sdk
The OpenAI Agents SDK documents function tools as local functions wrapped with a
schema through tool(), and the same tools can be attached to an Agent:
https://openai.github.io/openai-agents-js/guides/tools/.
The same guide documents MCP servers as attachable tool sources through
MCPServerStdio; Bilig keeps a provider-free smoke for that path too:
pnpm --dir examples/headless-workpaper run agent:openai-agents-sdk-mcp
import { Agent, RunContext, invokeFunctionTool, tool } from '@openai/agents'
import { z } from 'zod'
import { WorkPaper } from '@bilig/headless'
const workbook = WorkPaper.buildFromSheets({
Inputs: [
['Metric', 'Value'],
['Qualified opportunities', 20],
['Win rate', 0.25],
['Average ARR', 12000],
],
Summary: [
['Metric', 'Value'],
['Expected ARR', '=Inputs!B2*Inputs!B3*Inputs!B4'],
],
})
const setWorkPaperInputCell = tool({
name: 'set_workpaper_input_cell',
description: 'Set one validated WorkPaper input cell and return formula readback.',
parameters: z.object({
sheetName: z.literal('Inputs'),
address: z.string().regex(/^[A-Z]+[1-9][0-9]*$/),
value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
}),
execute: async ({ sheetName, address, value }) => {
const sheet = workbook.getSheetId(sheetName)
const summarySheet = workbook.getSheetId('Summary')
if (sheet === undefined) {
throw new Error(`Unknown sheet: ${sheetName}`)
}
if (summarySheet === undefined) {
throw new Error('Summary sheet is missing')
}
const cell = workbook.simpleCellAddressFromString(address, sheet)
const summaryRange = workbook.simpleCellRangeFromString('Summary!A1:B2', summarySheet)
if (cell === undefined) {
throw new Error(`Invalid cell: ${sheetName}!${address}`)
}
if (summaryRange === undefined) {
throw new Error('Summary range is invalid')
}
const before = workbook.getRangeValues(summaryRange)
workbook.setCellContents(cell, value)
return {
editedCell: `${sheetName}!${address}`,
before,
after: workbook.getRangeValues(summaryRange),
}
},
})
const agent = new Agent({
name: 'WorkPaper verification agent',
instructions: 'Use WorkPaper tools and answer only from computed readback.',
tools: [setWorkPaperInputCell],
})
const result = await invokeFunctionTool({
tool: setWorkPaperInputCell,
runContext: new RunContext(),
input: JSON.stringify({
sheetName: 'Inputs',
address: 'B3',
value: 0.4,
}),
})
console.log(agent.name, result)
For a production adapter, use the full example instead of this short snippet:
examples/headless-workpaper/openai-agents-sdk-tool-smoke.ts.
It also verifies persisted formulas by exporting a WorkPaper document, restoring
it, and comparing the computed readback after restore.
Use this when your OpenAI Agents SDK app already manages MCP servers or when you want the same Bilig WorkPaper server available to other agent clients:
import { Agent, MCPServerStdio, RunContext, getAllMcpTools, invokeFunctionTool } from '@openai/agents'
const server = new MCPServerStdio({
name: 'bilig-workpaper-stdio',
fullCommand: 'npm run --silent agent:mcp-stdio',
cwd: 'examples/headless-workpaper',
})
await server.connect()
try {
const agent = new Agent({
name: 'WorkPaper MCP verification agent',
instructions: 'Answer only from computed WorkPaper MCP readback.',
mcpServers: [server],
})
const runContext = new RunContext()
const tools = await getAllMcpTools({
mcpServers: [server],
runContext,
agent,
convertSchemasToStrict: true,
})
const setInput = tools.find((tool) => tool.name === 'set_workpaper_input_cell')
if (setInput === undefined) {
throw new Error('Missing set_workpaper_input_cell')
}
const result = await invokeFunctionTool({
tool: setInput,
runContext,
input: JSON.stringify({ sheetName: 'Inputs', address: 'B3', value: 0.4 }),
})
console.log(result)
} finally {
await server.close()
}
The maintained proof file is
examples/headless-workpaper/openai-agents-sdk-mcp-smoke.ts.
It starts the Bilig stdio server, lists MCP tools, converts them into Agents SDK
function tools with getAllMcpTools(), invokes set_workpaper_input_cell, and
asserts formula readback plus JSON restore.
The smoke output includes this shape:
{
"apiShape": "OpenAI Agents SDK Agent -> tool() -> invokeFunctionTool()",
"package": "@openai/agents",
"agentName": "WorkPaper verification agent",
"toolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
"writeResult": {
"editedCell": "Inputs!B3",
"before": { "expectedArr": 60000, "targetGap": -34000 },
"after": { "expectedArr": 96000, "targetGap": 5600 },
"checks": {
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
}
The MCP smoke output includes this shape:
{
"apiShape": "OpenAI Agents SDK Agent -> MCPServerStdio -> getAllMcpTools() -> invokeFunctionTool()",
"package": "@openai/agents",
"agentName": "WorkPaper MCP verification agent",
"mcpServerName": "bilig-workpaper-stdio",
"rawMcpToolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
"functionToolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
"writeResult": {
"editedCell": "Inputs!B3",
"before": { "expectedArr": 60000, "targetGap": -34000 },
"after": { "expectedArr": 96000, "targetGap": 5600 },
"restored": { "expectedArr": 96000, "targetGap": 5600 },
"checks": {
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
}
Keep the workbook mutation closed-world: validate sheet names and A1 addresses, write one input at a time, recalculate through WorkPaper, return computed readback, and persist only after the verification passes.