Auto-Generate MCP Tools from Your Backend Code in One Line
April 2026 · Sanjay Krishna Anbalagan
The tool description problem
Section titled “The tool description problem”Every MCP tool needs three things: a name, a description, and an input schema. If you’re building tools by hand, you’re writing and maintaining all three separately from the code that actually runs.
// You write this by hand...const tool = { name: 'assess_credit_risk', description: 'Evaluates credit risk by checking credit score, DTI ratio, and employment status. Returns risk tier and decision.', inputSchema: { type: 'object', properties: { creditScore: { type: 'number' }, dtiRatio: { type: 'number' }, employmentYears: { type: 'number' }, }, required: ['creditScore', 'dtiRatio', 'employmentYears'], },};
// ...and separately maintain the code that does the workfunction assessCreditRisk(input) { // 200 lines of business logic}The description drifts from the code. You add a new stage but forget to update the tool description. You rename a field but the schema still says the old name. The LLM gets confused because the tool it’s calling doesn’t match what it was told.
FootPrintJS solves this with self-describing graphs. The flowchart is the tool. The name, description, and schema are generated from the graph structure — always in sync with the code that runs.
One line: .toMCPTool()
Section titled “One line: .toMCPTool()”import { flowChart, decide, FlowChartExecutor } from 'footprintjs';
interface CreditState { creditScore: number; dtiRatio: number; employmentYears: number; riskTier: string; decision: string; reasons: string[];}
const chart = flowChart<CreditState>('ReceiveApplication', async (scope) => { // Input is already on scope from the caller }, 'assess-credit-risk', { description: 'Evaluates credit risk by checking credit score, DTI ratio, and employment status. Returns risk tier and decision.', }) .addFunction('CheckCredit', async (scope) => { scope.riskTier = scope.creditScore >= 700 ? 'low' : scope.creditScore >= 620 ? 'medium' : 'high'; }, 'check-credit') .addFunction('CheckDTI', async (scope) => { if (scope.dtiRatio > 0.43) { scope.reasons = [...(scope.reasons || []), 'DTI exceeds 43%']; } }, 'check-dti') .addDeciderFunction('Route', (scope) => { return decide(scope, [ { when: { riskTier: { eq: 'high' } }, then: 'reject', label: 'High risk' }, ], 'approve'); }, 'route', 'Route based on overall risk assessment') .addFunctionBranch('reject', 'Reject', async (scope) => { scope.decision = 'REJECTED'; }) .addFunctionBranch('approve', 'Approve', async (scope) => { scope.decision = 'APPROVED'; }) .setDefault('approve') .end() // The contract declares the input schema toMCPTool() reads .contract({ input: { type: 'object', properties: { creditScore: { type: 'number' }, dtiRatio: { type: 'number' }, employmentYears: { type: 'number' }, }, required: ['creditScore', 'dtiRatio', 'employmentYears'], }, }) .build();
// One line — tool descriptor generated from the graphconst tool = chart.toMCPTool();tool now contains:
{ "name": "assess-credit-risk", "description": "Evaluates credit risk by checking credit score, DTI ratio, and employment status. Returns risk tier and decision.", "inputSchema": { "type": "object", "properties": { "creditScore": { "type": "number" }, "dtiRatio": { "type": "number" }, "employmentYears": { "type": "number" } }, "required": ["creditScore", "dtiRatio", "employmentYears"] }}The name comes from the chart’s root id (sanitized to the MCP name allowlist), the description from the chart description, and the inputSchema from the contract. They all live next to the code that runs — change the chart and the descriptor changes with it. (When no contract is declared, inputSchema defaults to the MCP no-parameter form { "type": "object", "properties": {}, "additionalProperties": false }.)
Register with any MCP server
Section titled “Register with any MCP server”The output of .toMCPTool() is a standard MCP tool descriptor. Register it with any MCP server implementation:
// With the Anthropic SDK directlyconst anthropicTool = { name: tool.name, description: tool.description, input_schema: tool.inputSchema,};
// Pass to Claudeconst response = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', tools: [anthropicTool], messages: [{ role: 'user', content: 'Assess credit risk for score 580, DTI 0.6, 1yr employment' }],});When Claude calls the tool, execute the flowchart and return the result with the causal trace:
// Handle the tool callconst toolInput = response.content[0].input; // { creditScore: 580, dtiRatio: 0.6, employmentYears: 1 }
// Run the chart with the tool input; the executor exposes the narrative.const executor = new FlowChartExecutor(chart);await executor.run({ input: toolInput });
const state = executor.getSnapshot().sharedState as CreditState;const narrative = executor.getNarrativeEntries().map((e) => e.text).join('\n');
// Return to Claude: the decision + whyconst toolResult = { decision: state.decision, // "REJECTED" reasons: state.reasons, // ["DTI exceeds 43%"] trace: narrative, // Full causal trace};Claude now has the structured trace. When the user asks “why was I rejected?”, Claude reads the trace and gives a precise, non-hallucinated explanation.
Also: .toOpenAPI()
Section titled “Also: .toOpenAPI()”Same graph, different output format. Generate an OpenAPI 3.1 spec for REST API documentation:
const spec = chart.toOpenAPI({ title: 'Credit Risk Assessment', version: '1.0.0', description: 'Automated credit risk evaluation pipeline',});
// spec is a full OpenAPI 3.1 document// Import into Swagger UI, Postman, or any API documentation toolOne flowchart, two output formats — MCP for AI agents, OpenAPI for humans and API consumers.
With agentfootprint: full agent integration
Section titled “With agentfootprint: full agent integration”The adapter below is a complete, local example: the chart provides the tool metadata and the executor provides its implementation. It does not call a model or require API credentials.
import { flowChart, FlowChartExecutor } from 'footprintjs';
interface CreditInput { creditScore: number; dtiRatio: number; employmentYears: number;}
const chart = flowChart<{ decision: string }>('AssessCredit', (scope) => { const input = scope.$getArgs<CreditInput>(); scope.decision = input.creditScore >= 620 && input.dtiRatio <= 0.43 && input.employmentYears >= 1 ? 'APPROVED' : 'REJECTED';}, 'assess-credit-risk', { description: 'Assess credit score, debt-to-income ratio, and employment history. Return a decision and its trace.',}) .contract({ input: { type: 'object', properties: { creditScore: { type: 'number' }, dtiRatio: { type: 'number' }, employmentYears: { type: 'number' }, }, required: ['creditScore', 'dtiRatio', 'employmentYears'], }, }) .build();
export const creditTool = { ...chart.toMCPTool(), // Tool-call input is untrusted; the chart's contract validates it before execution. async execute(input: unknown): Promise<{ decision: string; trace: string[] }> { const executor = new FlowChartExecutor(chart); executor.enableNarrative(); await executor.run({ input }); const decision = executor.getSnapshot().sharedState.decision; if (typeof decision !== 'string') throw new Error('The chart did not produce a decision'); return { decision, trace: executor.getNarrativeEntries().map((e) => e.text), }; },};
console.log(await creditTool.execute({ creditScore: 580, dtiRatio: 0.6, employmentYears: 1 }));// { decision: 'REJECTED', trace: [...] }In an AgentFootprint application, pass this descriptor and its execute function to defineTool, then register the resulting tool with the agent builder’s .tool(...) method. Provider configuration, credentials, and agent.run(...) belong to that application. When the agent invokes the tool, it receives the JSON-serializable decision and narrative returned here.
Why this matters
Section titled “Why this matters”For AI engineers: Your tools stay in sync with your code. Add a stage, the tool description updates. Rename a field, the schema updates. No drift.
For compliance teams: The causal trace is auto-generated, not hand-written. It’s a faithful record of what the code actually did — auditable, reproducible, tamper-evident.
For product teams: LLMs give better explanations because they have structured traces instead of scattered logs. User trust increases because the explanations are accurate.
Try the live demo
Section titled “Try the live demo”We have a live demo where Claude calls a credit-decision flowchart as an MCP tool. Enter your API key, ask a question, and watch the tool call happen in real time:
Live MCP Demo — see .toMCPTool() in action
npm install footprintjs # the enginenpm install agentfootprint # the agent framework (optional)- Interactive Playground — 37+ samples
- Documentation — full API reference
- GitHub — MIT licensed, zero dependencies