Skip to content

Auto-Generate MCP Tools from Your Backend Code in One Line

April 2026 · Sanjay Krishna Anbalagan


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 work
function 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.

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 graph
const 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 }.)

The output of .toMCPTool() is a standard MCP tool descriptor. Register it with any MCP server implementation:

// With the Anthropic SDK directly
const anthropicTool = {
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
};
// Pass to Claude
const 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 call
const 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 + why
const 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.

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 tool

One 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.

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.

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

Terminal window
npm install footprintjs # the engine
npm install agentfootprint # the agent framework (optional)