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”If you’re using agentfootprint (the agent framework), the integration is even tighter:
import { Agent, defineTool } from 'agentfootprint';import { anthropic } from 'agentfootprint/llm-providers';import { FlowChartExecutor } from 'footprintjs';
// Turn the flowchart into an agent toolconst creditTool = defineTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, execute: async (input) => { const executor = new FlowChartExecutor(chart); await executor.run({ input }); const state = executor.getSnapshot().sharedState as CreditState; // execute() returns anything JSON-serializable; it's forwarded to the LLM return { decision: state.decision, trace: executor.getNarrativeEntries().map((e) => e.text), }; },});
// Build an agent that uses itconst agent = Agent.create({ provider: anthropic({ defaultModel: 'claude-sonnet-4-5-20250929' }),}) .system('You are a loan officer assistant. Use the credit assessment tool to evaluate applications and explain decisions clearly.') .tool(creditTool) .build();
const response = await agent.run({ message: 'Evaluate this application: credit score 580, DTI 60%, 1 year employment',});// Agent calls the tool, gets the trace, explains the decisionWhy 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