Error Handling
MCP Fusion uses a unified XML error protocol designed for both human debugging and LLM agent self-correction. Every error — from simple failures to complex validation issues — is wrapped in structured XML that agents can parse, structurally perceive, and act on deterministically.
Why XML?
LLMs are pre-trained on millions of XML documents. Structured XML errors are:
- Parseable — agents extract error codes, messages, and recovery instructions without ambiguity
- Hierarchical — complex errors (multi-field validation) are naturally nested
- Self-healing —
<recovery>tags give the agent an exact next step, reducing retry loops by 60-80%
The Error Hierarchy
error() → Simple error with XML envelope
required() → Missing field with recovery hint
toolError() → Self-healing error with code + actions
Validation → Automatic per-field correction (generated by pipeline)
Routing → Automatic action resolution (generated by pipeline)
Result<T> → Pipeline-oriented success/failure compositionSimple Errors
Use error() for straightforward failures:
import { error } from '@vinkius-core/mcp-fusion';
handler: async (ctx, args) => {
const project = await ctx.db.projects.findUnique(args.id);
if (!project) return error(`Project "${args.id}" not found`);
return success(project);
}What the LLM receives:
<tool_error>
<message>Project "proj_xyz" not found</message>
</tool_error>Missing Field Errors
required() is a convenience shortcut that includes a recovery instruction:
import { required } from '@vinkius-core/mcp-fusion';
handler: async (ctx, args) => {
if (!args.workspace_id) return required('workspace_id');
// ...
}What the LLM receives:
<tool_error code="MISSING_REQUIRED_FIELD">
<message>Required field "workspace_id" is missing.</message>
<recovery>Provide the "workspace_id" parameter and retry.</recovery>
</tool_error>The code attribute lets agents match on MISSING_REQUIRED_FIELD programmatically, while <recovery> tells them exactly what to do.
Self-Healing Errors (Agent Experience)
toolError() provides structured recovery instructions so LLM agents can self-correct instead of hallucinating:
import { toolError } from '@vinkius-core/mcp-fusion';
handler: async (ctx, args) => {
const project = await ctx.db.get(args.project_id);
if (!project) {
return toolError('ProjectNotFound', {
message: `Project '${args.project_id}' does not exist.`,
suggestion: 'Call projects.list first to get valid IDs, then retry.',
availableActions: ['projects.list'],
});
}
return success(project);
}What the LLM receives:
<tool_error code="ProjectNotFound">
<message>Project 'proj_xyz' does not exist.</message>
<recovery>Call projects.list first to get valid IDs, then retry.</recovery>
<available_actions>projects.list</available_actions>
</tool_error>This structured format helps the agent:
- Structurally perceive the error — via the
codeattribute - Know what to do — via
<recovery> - Know which actions exist — via
<available_actions>
Minimal Usage
// No suggestion or available actions — just a code + message
return toolError('RateLimited', {
message: 'Too many requests. Wait 30 seconds.',
});<tool_error code="RateLimited">
<message>Too many requests. Wait 30 seconds.</message>
</tool_error>When to Use Each
| Helper | Use Case | LLM Benefit |
|---|---|---|
error() | Generic failures, auth errors | Basic error signal |
required() | Missing arguments | Tells agent which field to add |
toolError() | Recoverable failures | Full self-healing with next steps |
Automatic Validation Errors
The framework automatically formats Zod validation errors. You don't write any formatting code — this is generated by the execution pipeline when the LLM sends invalid arguments.
Per-Field Correction
When the LLM sends arguments that fail Zod validation, it receives a structured, actionable correction prompt:
<validation_error action="users/create">
<field name="email">Invalid email. You sent: 'bad-email'. Expected: a valid email address (e.g. user@example.com).</field>
<field name="role">Invalid enum value. Expected 'admin' | 'user', received 'superadmin'. You sent: 'superadmin'. Valid options: 'admin', 'user'.</field>
<recovery>Fix the fields above and call the tool again. Do not explain the error.</recovery>
</validation_error>Key design decisions:
actionattribute — the agent knows which action failed- Per-field
You sent:values — the agent sees exactly what it passed - Expected types/formats — tells the agent what to send instead
- Anti-apology
<recovery>— instructs the agent to retry immediately, not explain
Unknown Fields (.strict())
When the LLM sends fields not declared in the schema, they are explicitly rejected (not silently stripped):
<validation_error action="billing/create">
<field name="(root)">Unrecognized key(s) in object: 'hallucinated_param', 'admin_override'. Remove or correct unrecognized fields: 'hallucinated_param', 'admin_override'. Check for typos.</field>
<recovery>Fix the fields above and call the tool again. Do not explain the error.</recovery>
</validation_error>This teaches the agent which fields are valid, enabling self-correction on retry.
Automatic Routing Errors
The execution pipeline generates routing errors when the LLM omits or misspells the discriminator field.
Missing Discriminator
<tool_error code="MISSING_DISCRIMINATOR">
<message>The required field "action" is missing.</message>
<available_actions>list, create, delete</available_actions>
<recovery>Add the "action" field and call the tool again.</recovery>
</tool_error>Unknown Action
<tool_error code="UNKNOWN_ACTION">
<message>The action "destory" does not exist.</message>
<available_actions>list, create, delete</available_actions>
<recovery>Choose a valid action from available_actions and call the tool again.</recovery>
</tool_error>Unified XML Protocol
Every error in the system follows the same structural contract:
| Error Type | Source | Root Element | When |
|---|---|---|---|
error() | Your handler | <tool_error> | Generic failures |
required() | Your handler | <tool_error code="MISSING_REQUIRED_FIELD"> | Missing arguments |
toolError() | Your handler | <tool_error code="..."> | Recoverable business errors |
| Validation | Automatic | <validation_error action="..."> | Bad args or unknown fields |
| Routing | Automatic | <tool_error code="MISSING_DISCRIMINATOR|UNKNOWN_ACTION|UNKNOWN_TOOL"> | Missing/invalid action or tool |
All error responses set isError: true in the MCP protocol, allowing the LLM runtime to distinguish errors from successful tool calls.
XML Security
All user-controlled data in error outputs is automatically escaped to prevent XML injection. Element content escapes & and < (preserving > for LLM readability in expressions like >= 1). Attribute values escape all five XML special characters (&, <, >, ", ').
Result Monad — Pipeline Composition
For complex multi-step operations, use the Result<T> monad to compose error handling without try/catch:
import { succeed, fail, error, type Result } from '@vinkius-core/mcp-fusion';
function findUser(id: string): Result<User> {
const user = db.users.get(id);
return user ? succeed(user) : fail(error(`User "${id}" not found`));
}
function checkPermission(user: User, action: string): Result<User> {
return user.can(action)
? succeed(user)
: fail(error(`User "${user.id}" cannot ${action}`));
}
// Pipeline composition
handler: async (ctx, args) => {
const user = findUser(args.user_id);
if (!user.ok) return user.response; // Early exit
const authorized = checkPermission(user.value, 'delete');
if (!authorized.ok) return authorized.response; // Early exit
await ctx.db.projects.delete(args.project_id);
return success('Deleted');
}Pipeline Pattern Summary
const step1 = someOperation();
if (!step1.ok) return step1.response; // ← Failure short-circuits
const step2 = nextOperation(step1.value); // ← Success narrows type
if (!step2.ok) return step2.response;
return success(step2.value); // ← Final successSee also: Result Monad for the complete API reference and advanced patterns.
Combining with Middleware
Middleware can handle cross-cutting error concerns:
const errorBoundary: MiddlewareFn<AppContext> = async (ctx, args, next) => {
try {
return await next();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return toolError('UnhandledException', {
message,
suggestion: 'This is an unexpected error. Please report it.',
});
}
};
const tool = createTool<AppContext>('projects')
.use(errorBoundary)
.action({ name: 'list', handler: listProjects });Best Practices
- Prefer
toolError()overerror()for any failure the LLM could recover from - Use
Result<T>for multi-step pipelines to avoid nested try/catch - Include
availableActionsso the LLM knows which tool calls can fix the issue - Keep error messages concise — LLMs process shorter text more accurately
- Never expose internal stack traces — use error codes like
'DatabaseError', not raw SQL errors - Trust the pipeline — validation and routing errors are handled automatically with optimal formatting