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”

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 tool
const 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 it
const 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 decision

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)