Skip to content

Self-describing APIs

A footprintjs chart knows its own structure. Call .toOpenAPI() to get an OpenAPI 3.1 spec, or .toMCPTool() to get an MCP tool description — no separate docs to write or maintain.

Use .contract() on the builder (before .build()) to attach input/output schemas and an output mapper:

import { flowChart, decide } from 'footprintjs';
import { z } from 'zod';
interface OrderState {
subtotal?: number;
tax?: number;
total?: number;
shippingMethod?: string;
status?: string;
}
const chart = flowChart<OrderState>('ReceiveOrder', async (scope) => {
scope.$log('Order received');
}, 'receive-order')
.addFunction('CalculateTotal', async (scope) => {
const { quantity, unitPrice } = scope.$getArgs<{ quantity: number; unitPrice: number }>();
scope.subtotal = quantity * unitPrice;
scope.tax = Math.round(scope.subtotal * 0.08 * 100) / 100;
scope.total = scope.subtotal + scope.tax;
}, 'calculate-total')
.addDeciderFunction('ClassifyOrder', (scope) => {
return decide(scope, [
{ when: { total: { gt: 100 } }, then: 'large', label: 'Large order' },
], 'small');
}, 'classify-order', 'Route by order size')
.addFunctionBranch('large', 'ProcessLargeOrder', async (scope) => {
scope.shippingMethod = 'express';
scope.status = 'approved -- express shipping';
})
.addFunctionBranch('small', 'ProcessSmallOrder', async (scope) => {
scope.shippingMethod = 'standard';
scope.status = 'approved -- standard shipping';
})
.setDefault('small')
.end()
.contract({
input: z.object({
item: z.string().describe('Product name'),
quantity: z.number().describe('Number of units'),
unitPrice: z.number().describe('Price per unit in USD'),
}),
output: z.object({
total: z.number().describe('Order total including tax'),
shippingMethod: z.enum(['standard', 'express']),
status: z.string(),
}),
mapper: (scope) => ({
total: scope.total,
shippingMethod: scope.shippingMethod,
status: scope.status,
}),
})
.build();

Call chart.toOpenAPI() on the compiled chart. The result is cached:

const spec = chart.toOpenAPI({
title: 'Order Processing API',
version: '1.0.0',
path: '/orders/process',
});
console.log(JSON.stringify(spec, null, 2));
// {
// "openapi": "3.1.0",
// "info": { "title": "Order Processing API", "version": "1.0.0" },
// "paths": {
// "/orders/process": {
// "post": {
// "requestBody": { "content": { "application/json": { "schema": { ... } } } },
// "responses": { "200": { "content": { "application/json": { "schema": { ... } } } } }
// }
// }
// }
// }

toOpenAPI() options:

OptionDefaultDescription
titleChart descriptionAPI title
version'1.0.0'API version string
descriptionChart descriptionAPI description
path'/<root-stage-name>'HTTP path

chart.toMCPTool() returns an MCP-compatible tool definition that can be registered with any MCP server:

const tool = chart.toMCPTool();
console.log(tool);
// {
// name: 'receiveorder',
// description: '1. ReceiveOrder\n2. CalculateTotal\n3. ClassifyOrder ...',
// inputSchema: { type: 'object', properties: { item: ..., quantity: ..., unitPrice: ... } }
// }

The name is derived from the root stage name (lowercased, spaces replaced with underscores). The description is the numbered step list generated from stage descriptions you provide in the builder.

.contract() also accepts plain JSON Schema objects if you don’t use Zod:

.contract({
input: {
type: 'object',
properties: {
quantity: { type: 'number', description: 'Number of units' },
unitPrice: { type: 'number', description: 'Price per unit' },
},
required: ['quantity', 'unitPrice'],
},
output: {
type: 'object',
properties: {
total: { type: 'number' },
status: { type: 'string' },
},
},
mapper: (scope) => ({ total: scope.total, status: scope.status }),
})

Run this example in the browser: