diff --git a/examples/README.md b/examples/README.md index a69801d31..cdbc64520 100644 --- a/examples/README.md +++ b/examples/README.md @@ -116,6 +116,7 @@ Create a multi-agent research workflow where different AI agents collaborate to - [Langfuse](./with-langfuse) — Send traces and metrics to Langfuse for observability. - [Feedback Templates](./with-feedback) — Configure per-agent feedback templates for thumbs, numeric, and categorical feedback. - [Live Evals](./with-live-evals) — Run online evaluations against prompts/agents during development. +- [Bilig WorkPaper (MCP)](./with-bilig-workpaper-mcp) — Edit formula-backed WorkPaper inputs, recalculate outputs, and verify persisted JSON through MCP tools. - [MCP Basics](./with-mcp) — Connect to MCP servers and call tools from an agent. - [MCP Elicitation](./with-mcp-elicitation) — Handle `elicitation/create` requests from MCP tools with per-request handlers. - [MCP Server](./with-mcp-server) — Implement and run a local MCP server that exposes custom tools. diff --git a/examples/with-bilig-workpaper-mcp/.env.example b/examples/with-bilig-workpaper-mcp/.env.example new file mode 100644 index 000000000..7e24174e9 --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY=your-openai-api-key + diff --git a/examples/with-bilig-workpaper-mcp/.gitignore b/examples/with-bilig-workpaper-mcp/.gitignore new file mode 100644 index 000000000..82760ab27 --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/.gitignore @@ -0,0 +1,6 @@ +.env +.voltagent/ +dist/ +node_modules/ +pricing.workpaper.json + diff --git a/examples/with-bilig-workpaper-mcp/README.md b/examples/with-bilig-workpaper-mcp/README.md new file mode 100644 index 000000000..60e126b58 --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/README.md @@ -0,0 +1,40 @@ +# Bilig WorkPaper MCP + +Use VoltAgent with a local Bilig WorkPaper MCP server for formula-backed workbook automation. The example starts `@bilig/workpaper`, edits an input cell, recalculates dependent formulas, reads the computed result, and verifies persisted WorkPaper JSON. + +## Try Example + +```bash +npm create voltagent-app@latest -- --example with-bilig-workpaper-mcp +``` + +## Run The Proof + +The proof script does not require an LLM key. It uses VoltAgent's `MCPConfiguration` to call the Bilig MCP tools directly. + +```bash +pnpm install +pnpm proof +``` + +Expected result: + +```json +{ + "ok": true, + "package": "@bilig/workpaper@0.154.0", + "recalculated": { + "summaryB3": 96000 + }, + "persisted": true +} +``` + +## Run The Agent + +```bash +cp .env.example .env +pnpm dev +``` + +Open the VoltAgent console and ask the agent to inspect the workbook, set `Inputs!B3` to `0.4`, and verify `Summary!B3`. The important pattern is read, edit, recalculate, read back, and export the persisted WorkPaper document. diff --git a/examples/with-bilig-workpaper-mcp/package.json b/examples/with-bilig-workpaper-mcp/package.json new file mode 100644 index 000000000..49536033b --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/package.json @@ -0,0 +1,32 @@ +{ + "name": "voltagent-example-with-bilig-workpaper-mcp", + "description": "VoltAgent example using Bilig WorkPaper MCP tools for formula-backed workbook readback.", + "version": "0.1.0", + "dependencies": { + "@voltagent/cli": "^0.1.21", + "@voltagent/core": "^2.7.5", + "@voltagent/internal": "^1.0.3", + "ai": "^6.0.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.2.1", + "tsx": "^4.21.0", + "typescript": "^5.8.2" + }, + "main": "dist/index.js", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/VoltAgent/voltagent.git", + "directory": "examples/with-bilig-workpaper-mcp" + }, + "scripts": { + "build": "tsc", + "dev": "tsx watch --env-file=.env ./src", + "proof": "tsx ./src/proof.ts", + "start": "node dist/index.js", + "volt": "volt" + }, + "type": "module" +} diff --git a/examples/with-bilig-workpaper-mcp/src/index.ts b/examples/with-bilig-workpaper-mcp/src/index.ts new file mode 100644 index 000000000..12ce44f53 --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/src/index.ts @@ -0,0 +1,25 @@ +import VoltAgent, { Agent } from "@voltagent/core"; +import { biligWorkPaperPackage, createBiligWorkPaperMcpConfig } from "./mcp.js"; + +const mcpConfig = createBiligWorkPaperMcpConfig(); +const tools = await mcpConfig.getTools(); + +const agent = new Agent({ + name: "Bilig WorkPaper Agent", + instructions: [ + "You help users inspect and edit formula-backed WorkPaper files through MCP tools.", + `The configured server runs ${biligWorkPaperPackage}.`, + "Before changing a cell, list sheets and read the relevant input and output cells.", + "After set_cell_contents, verify the dependent result with read_cell or get_cell_display_value.", + "Do not claim success from a write alone; use recalculated readback and persisted JSON proof.", + ].join(" "), + model: "openai/gpt-4o-mini", + tools, + markdown: true, +}); + +new VoltAgent({ + agents: { + agent, + }, +}); diff --git a/examples/with-bilig-workpaper-mcp/src/mcp.ts b/examples/with-bilig-workpaper-mcp/src/mcp.ts new file mode 100644 index 000000000..74348f8d5 --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/src/mcp.ts @@ -0,0 +1,29 @@ +import path from "node:path"; +import { MCPConfiguration } from "@voltagent/core"; + +export const biligWorkPaperPackage = "@bilig/workpaper@0.154.0"; + +export function createBiligWorkPaperMcpConfig( + workpaperPath = path.resolve("pricing.workpaper.json"), +) { + return new MCPConfiguration({ + servers: { + bilig: { + type: "stdio", + command: "npm", + args: [ + "exec", + "--yes", + "--package", + biligWorkPaperPackage, + "--", + "bilig-workpaper-mcp", + "--workpaper", + workpaperPath, + "--init-demo-workpaper", + "--writable", + ], + }, + }, + }); +} diff --git a/examples/with-bilig-workpaper-mcp/src/proof.ts b/examples/with-bilig-workpaper-mcp/src/proof.ts new file mode 100644 index 000000000..7a94adcea --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/src/proof.ts @@ -0,0 +1,266 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { safeStringify } from "@voltagent/internal/utils"; +import { biligWorkPaperPackage, createBiligWorkPaperMcpConfig } from "./mcp.js"; + +type JsonRecord = Record; +type ExecutableTool = { + name: string; + execute?: (args: JsonRecord) => Promise | unknown; +}; +type RequiredExecutableTool = ExecutableTool & { + execute: (args: JsonRecord) => Promise | unknown; +}; + +const expectedToolSuffixes = [ + "list_sheets", + "read_range", + "read_cell", + "set_cell_contents", + "get_cell_display_value", + "export_workpaper_document", + "validate_formula", +]; + +function isRecord(value: unknown): value is JsonRecord { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function requireRecord(value: unknown, label: string): JsonRecord { + if (isRecord(value)) { + return value; + } + + throw new Error(`${label} was not an object: ${safeStringify(value)}`); +} + +function requireNumber(value: unknown, label: string): number { + if (typeof value === "number") { + return value; + } + + throw new Error(`${label} was not a number: ${safeStringify(value)}`); +} + +function requireString(value: unknown, label: string): string { + if (typeof value === "string") { + return value; + } + + throw new Error(`${label} was not a string: ${safeStringify(value)}`); +} + +function requireBoolean(value: unknown, label: string): boolean { + if (typeof value === "boolean") { + return value; + } + + throw new Error(`${label} was not a boolean: ${safeStringify(value)}`); +} + +function toolSuffix(toolName: string): string { + const prefix = "bilig_"; + return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName; +} + +function findTool(tools: readonly ExecutableTool[], suffix: string): RequiredExecutableTool { + const tool = tools.find((candidate) => toolSuffix(candidate.name) === suffix); + if (!tool || typeof tool.execute !== "function") { + throw new Error(`Missing executable Bilig MCP tool: ${suffix}`); + } + + return tool as RequiredExecutableTool; +} + +function parseToolText(text: string, label: string): JsonRecord { + try { + return requireRecord(JSON.parse(text), `${label} text payload`); + } catch (error) { + throw new Error( + `${label} did not contain JSON tool output: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +} + +function structuredContent(value: unknown, label: string): JsonRecord { + const result = requireRecord(value, label); + const direct = result.structuredContent; + + if (isRecord(direct)) { + return direct; + } + + const content = result.content; + if (Array.isArray(content)) { + const textPart = content.find((part) => isRecord(part) && typeof part.text === "string"); + if (isRecord(textPart) && typeof textPart.text === "string") { + return parseToolText(textPart.text, label); + } + } + + throw new Error(`${label} missing structured MCP output: ${safeStringify(result)}`); +} + +async function callTool( + tools: readonly ExecutableTool[], + suffix: string, + args: JsonRecord, +): Promise { + const tool = findTool(tools, suffix); + return structuredContent(await tool.execute(args), suffix); +} + +function cellValue(cell: JsonRecord, label: string): number { + if (typeof cell.value === "number") { + return cell.value; + } + + const taggedValue = requireRecord(cell.value, `${label}.value`); + return requireNumber(taggedValue.value, `${label}.value.value`); +} + +async function runProofWithWorkpaper(workpaperPath: string) { + const mcpConfig = createBiligWorkPaperMcpConfig(workpaperPath); + let tools: ExecutableTool[] = []; + let afterValue = 0; + + try { + tools = (await mcpConfig.getTools()) as ExecutableTool[]; + const discoveredSuffixes = tools.map((tool) => toolSuffix(tool.name)).sort(); + const missingTools = expectedToolSuffixes.filter( + (suffix) => !discoveredSuffixes.includes(suffix), + ); + if (missingTools.length > 0) { + throw new Error(`Bilig MCP tools missing: ${missingTools.join(", ")}`); + } + + const sheets = await callTool(tools, "list_sheets", {}); + const sheetList = sheets.sheets; + if (!Array.isArray(sheetList) || sheetList.length < 2) { + throw new Error(`Expected demo WorkPaper sheets, received: ${safeStringify(sheetList)}`); + } + requireBoolean(sheets.writable, "list_sheets.writable"); + + const before = await callTool(tools, "read_cell", { + sheetName: "Summary", + address: "B3", + }); + const beforeValue = cellValue(before, "before"); + + const validation = await callTool(tools, "validate_formula", { + formula: "=SUM(1,2)", + }); + if (validation.valid !== true) { + throw new Error(`Formula validation failed: ${safeStringify(validation)}`); + } + + const write = await callTool(tools, "set_cell_contents", { + sheetName: "Inputs", + address: "B3", + value: 0.4, + }); + const checks = requireRecord(write.checks, "set_cell_contents.checks"); + const persistence = requireRecord(write.persistence, "set_cell_contents.persistence"); + if (write.editedCell !== "Inputs!B3") { + throw new Error(`Unexpected edited cell: ${safeStringify(write.editedCell)}`); + } + if (checks.persisted !== true || checks.restoredMatchesAfter !== true) { + throw new Error(`Write did not persist and restore cleanly: ${safeStringify(checks)}`); + } + if ( + persistence.persisted !== true || + requireNumber(persistence.serializedBytes, "serializedBytes") <= 0 + ) { + throw new Error(`Persistence proof failed: ${safeStringify(persistence)}`); + } + + const after = await callTool(tools, "read_cell", { + sheetName: "Summary", + address: "B3", + }); + afterValue = cellValue(after, "after"); + + const display = await callTool(tools, "get_cell_display_value", { + sheetName: "Summary", + address: "B3", + }); + const displayValue = requireString(display.displayValue, "display.displayValue"); + + const exported = await callTool(tools, "export_workpaper_document", { + includeConfig: true, + }); + const exportedBytes = requireNumber(exported.serializedBytes, "exported.serializedBytes"); + + if ( + beforeValue !== 60000 || + afterValue !== 96000 || + displayValue !== "96000" || + exportedBytes <= 0 + ) { + throw new Error( + `Unexpected WorkPaper proof values: ${safeStringify({ + beforeValue, + afterValue, + displayValue, + exportedBytes, + })}`, + ); + } + } finally { + await mcpConfig.disconnect(); + } + + const restartedConfig = createBiligWorkPaperMcpConfig(workpaperPath); + try { + const restartedTools = (await restartedConfig.getTools()) as ExecutableTool[]; + const restarted = await callTool(restartedTools, "read_cell", { + sheetName: "Summary", + address: "B3", + }); + const restartedValue = cellValue(restarted, "restarted"); + if (restartedValue !== afterValue) { + throw new Error( + `Restarted readback did not match recalculated value: ${safeStringify({ + afterValue, + restartedValue, + })}`, + ); + } + + console.log( + safeStringify( + { + ok: true, + package: biligWorkPaperPackage, + discoveredTools: tools.map((tool) => tool.name).sort(), + recalculated: { + summaryB3: restartedValue, + }, + persisted: true, + }, + { indentation: 2 }, + ), + ); + } finally { + await restartedConfig.disconnect(); + } +} + +async function runProof() { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "voltagent-bilig-workpaper-")); + const workpaperPath = path.join(tempDir, "pricing.workpaper.json"); + + try { + await runProofWithWorkpaper(workpaperPath); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + +runProof().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : safeStringify(error)); + process.exitCode = 1; +}); diff --git a/examples/with-bilig-workpaper-mcp/tsconfig.json b/examples/with-bilig-workpaper-mcp/tsconfig.json new file mode 100644 index 000000000..cee90c6f3 --- /dev/null +++ b/examples/with-bilig-workpaper-mcp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eef3f09ce..ebd6c2518 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -744,6 +744,34 @@ importers: specifier: ^5.8.2 version: 5.9.2 + examples/with-bilig-workpaper-mcp: + dependencies: + '@voltagent/cli': + specifier: ^0.1.21 + version: link:../../packages/cli + '@voltagent/core': + specifier: ^2.7.5 + version: link:../../packages/core + '@voltagent/internal': + specifier: ^1.0.3 + version: link:../../packages/internal + ai: + specifier: ^6.0.0 + version: 6.0.3(zod@3.25.76) + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^24.2.1 + version: 24.6.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.8.2 + version: 5.9.3 + examples/with-cerbos: dependencies: '@cerbos/grpc':