Self-Healing Errors
Prerequisites
Install MCP Fusion before following this recipe: npm install @vinkius-core/mcp-fusion @modelcontextprotocol/sdk zod — or scaffold a project with npx fusion create.
- Introduction
- Simple Errors
- Structured Recovery with toolError()
- Business Logic Guards
- What the AI Sees
- Best Practices
Introduction
When an AI agent hits an error, the default behavior is to give up or hallucinate a workaround. MCP Fusion flips this by making errors self-healing — every error carries structured recovery instructions that tell the agent exactly what to do next.
Instead of a generic "Invoice not found" message that leaves the AI guessing, MCP Fusion produces machine-readable XML with a recovery path. The result: agents that fix their own mistakes on the first retry.
Simple Errors
For straightforward failures, use the error() helper. It wraps your message in the standard MCP isError: true response:
import { initFusion, error, success } from '@vinkius-core/mcp-fusion';
const f = initFusion<AppContext>();
export const getProject = f.query('projects.get')
.describe('Get a project by ID')
.withString('id', 'Project ID')
.handle(async (input, ctx) => {
const project = await ctx.db.projects.findUnique({ where: { id: input.id } });
if (!project) return error(`Project "${input.id}" not found`);
return success(project);
});This works, but the AI only sees a text message. It doesn't know what to try next. For that, you need toolError().
Structured Recovery with toolError()
toolError() creates a rich error envelope with everything the AI needs to self-correct:
import { initFusion, toolError, success } from '@vinkius-core/mcp-fusion';
const f = initFusion<AppContext>();
export const getInvoice = f.query('billing.get_invoice')
.describe('Get an invoice by its ID')
.withString('id', 'Invoice ID')
.handle(async (input, ctx) => {
const invoice = await ctx.db.invoices.findUnique({
where: { id: input.id },
});
if (!invoice) {
return toolError('InvoiceNotFound', {
message: `Invoice "${input.id}" does not exist.`,
suggestion: 'Call billing.list_invoices first to find valid IDs.',
availableActions: ['billing.list_invoices'],
});
}
return success(invoice);
});The agent receives structured XML that it can parse and act on:
<tool_error code="InvoiceNotFound">
<message>Invoice "INV-999" does not exist.</message>
<recovery>Call billing.list_invoices first to find valid IDs.</recovery>
<available_actions>billing.list_invoices</available_actions>
</tool_error>The AI reads this and immediately calls billing.list_invoices — no human intervention needed. The error is self-healing.
Business Logic Guards
Real applications have complex business rules. toolError() shines when you need to guide the agent through multi-step validation:
export const chargeInvoice = f.mutation('billing.charge')
.describe('Process a payment for an invoice')
.withString('invoice_id', 'Invoice ID')
.withNumber('amount', 'Payment amount in cents')
.handle(async (input, ctx) => {
const invoice = await ctx.db.invoices.findUnique({
where: { id: input.invoice_id },
});
// Guard 1: Does the invoice exist?
if (!invoice) {
return toolError('InvoiceNotFound', {
message: `Invoice "${input.invoice_id}" not found.`,
suggestion: 'List invoices first, then retry with a valid ID.',
availableActions: ['billing.list_invoices'],
});
}
// Guard 2: Is it already settled?
if (invoice.status === 'paid') {
return toolError('AlreadyPaid', {
message: `Invoice "${input.invoice_id}" is already paid.`,
suggestion: 'No action needed. The invoice is settled.',
});
}
// Guard 3: Is the amount valid?
if (input.amount > invoice.amount_cents) {
return toolError('OverPayment', {
message: `Amount ${input.amount} exceeds invoice total ${invoice.amount_cents}.`,
suggestion: `Use amount: ${invoice.amount_cents} for full payment.`,
});
}
// All guards passed — execute the charge
await ctx.db.payments.create({
data: { invoiceId: input.invoice_id, amount: input.amount },
});
return success({ status: 'charged', amount: input.amount });
});Each guard returns a different error code with a specific recovery instruction. The AI never gets a generic "something went wrong" — it always knows the exact next step.
What the AI Sees
The difference in agent behavior is dramatic:
Without toolError() — the agent gives up:
AI: I need to charge invoice INV-999.
Tool: Error: Invoice not found.
AI: "I encountered an error trying to process the payment."With toolError() — the agent self-heals:
AI: I need to charge invoice INV-999.
Tool: <tool_error code="InvoiceNotFound">
<recovery>Call billing.list_invoices first.</recovery>
</tool_error>
AI: Let me find the correct invoice first.
→ calls billing.list_invoices
→ finds INV-042
→ calls billing.charge with INV-042
AI: "Payment of $450.00 processed successfully."Best Practices
Always include
suggestion— it's the most important field. Tell the agent what to do, not just what went wrong.Use
availableActionsfor navigation errors — when the agent used the wrong ID, point it to the listing action.Use specific error codes —
InvoiceNotFound,AlreadyPaid,OverPaymentare far more useful thanNOT_FOUND,BAD_REQUEST.Layer your guards — check existence first, then business rules, then authorization. Each returns a different recovery path.
Don't use
toolError()for validation errors — Zod.strict()already handles parameter validation with actionable messages. ReservetoolError()for business logic.