OpenAI Agents SDK WorkPaper Tools
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 three maintained integration shapes:
- function tools for apps that want WorkPaper in the same Node process.
- an MCP stdio server for apps that want the Agents SDK to discover WorkPaper
tools through
MCPServerStdioagainst a private local file or process. - a hosted Streamable HTTP MCP server for stateless smoke tests and tool
discovery through
MCPServerStreamableHttp.
The direct function-tool path gives the agent two ordinary function tools:
read_workpaper_summaryreads computed WorkPaper values and serialized cells.set_workpaper_input_cellwrites 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
OpenAI’s Agents SDK MCP guide also documents Streamable HTTP MCP servers, tool-list caching, server-prefixed names, and per-server tool filters: https://openai.github.io/openai-agents-js/guides/mcp/. Bilig keeps a no-key hosted smoke for the public stateless endpoint:
pnpm --dir examples/headless-workpaper run agent:openai-agents-sdk-hosted-mcp
Minimal Tool Shape
import { Agent, RunContext, invokeFunctionTool, tool } from '@openai/agents'
import { z } from 'zod'
import { WorkPaper } from '@bilig/workpaper'
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.
MCP Server Shape
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. Use stdio for private workbook state, writable file-backed runs, and offline CI:
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.
Hosted MCP Server Shape
Use this when you want a zero-install OpenAI Agents SDK smoke test against the public Bilig endpoint:
import { Agent, MCPServerStreamableHttp, RunContext, getAllMcpTools, invokeFunctionTool } from '@openai/agents'
const server = new MCPServerStreamableHttp({
name: 'bilig-workpaper-hosted',
url: 'https://bilig.proompteng.ai/mcp',
cacheToolsList: false,
timeout: 15_000,
})
await server.connect()
try {
const agent = new Agent({
name: 'WorkPaper hosted 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_cell_contents_and_readback')
if (setInput === undefined) {
throw new Error('Missing set_cell_contents_and_readback')
}
const result = await invokeFunctionTool({
tool: setInput,
runContext,
input: JSON.stringify({
sheetName: 'Inputs',
address: 'B3',
value: '=0.4',
readbackRange: 'Summary!A1:B4',
}),
})
console.log(result)
} finally {
await server.close()
}
The maintained proof file is
examples/headless-workpaper/openai-agents-sdk-hosted-mcp-smoke.ts.
It connects to https://bilig.proompteng.ai/mcp, lists all eight packaged
WorkPaper MCP tools, converts them with getAllMcpTools(), invokes
set_cell_contents_and_readback, and asserts Summary!B3 changes from 60000
to 96000 with restored readback still 96000.
The hosted endpoint is intentionally stateless. The proof asserts
persistence.persisted is false while still returning serialized bytes and
restored readback. Use the stdio server when the task must persist private
workbook edits.
When an agent mounts several local MCP servers, use the SDK’s
mcpConfig.includeServerInToolNames option to avoid duplicate tool names. For
large or remote tool lists, set cacheToolsList deliberately, and use
toolFilter when a run should expose only a safe subset of tools.
Expected Proof
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
}
}
}
The hosted MCP smoke output includes this shape:
{
"apiShape": "OpenAI Agents SDK Agent -> MCPServerStreamableHttp -> getAllMcpTools() -> invokeFunctionTool()",
"package": "@openai/agents",
"agentName": "WorkPaper hosted MCP verification agent",
"mcpServerName": "bilig-workpaper-hosted",
"remoteEndpoint": "https://bilig.proompteng.ai/mcp",
"transport": "streamable-http",
"stateless": true,
"rawMcpToolNames": [
"list_sheets",
"read_range",
"read_cell",
"set_cell_contents",
"set_cell_contents_and_readback",
"get_cell_display_value",
"export_workpaper_document",
"validate_formula"
],
"writeResult": {
"editedCell": "Inputs!B3",
"readbackRange": "Summary!A1:B4",
"beforeExpectedArr": 60000,
"afterExpectedArr": 96000,
"restoredExpectedArr": 96000,
"persistence": {
"persisted": false,
"serializedBytes": 1000
},
"checks": {
"persisted": false,
"readbackChanged": true,
"restoredReadbackMatchesAfter": 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.