Skip to content

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 composition

Simple Errors

Use error() for straightforward failures:

typescript
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:

xml
<tool_error>
<message>Project "proj_xyz" not found</message>
</tool_error>

Missing Field Errors

required() is a convenience shortcut that includes a recovery instruction:

typescript
import { required } from '@vinkius-core/mcp-fusion';

handler: async (ctx, args) => {
    if (!args.workspace_id) return required('workspace_id');
    // ...
}

What the LLM receives:

xml
<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:

typescript
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:

xml
<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:

  1. Structurally perceive the error — via the code attribute
  2. Know what to do — via <recovery>
  3. Know which actions exist — via <available_actions>

Minimal Usage

typescript
// No suggestion or available actions — just a code + message
return toolError('RateLimited', {
    message: 'Too many requests. Wait 30 seconds.',
});
xml
<tool_error code="RateLimited">
<message>Too many requests. Wait 30 seconds.</message>
</tool_error>

When to Use Each

HelperUse CaseLLM Benefit
error()Generic failures, auth errorsBasic error signal
required()Missing argumentsTells agent which field to add
toolError()Recoverable failuresFull 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:

xml
<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:

  • action attribute — 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):

xml
<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

xml
<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

xml
<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 TypeSourceRoot ElementWhen
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
ValidationAutomatic<validation_error action="...">Bad args or unknown fields
RoutingAutomatic<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:

typescript
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

typescript
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 success

See also: Result Monad for the complete API reference and advanced patterns.

Combining with Middleware

Middleware can handle cross-cutting error concerns:

typescript
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

  1. Prefer toolError() over error() for any failure the LLM could recover from
  2. Use Result<T> for multi-step pipelines to avoid nested try/catch
  3. Include availableActions so the LLM knows which tool calls can fix the issue
  4. Keep error messages concise — LLMs process shorter text more accurately
  5. Never expose internal stack traces — use error codes like 'DatabaseError', not raw SQL errors
  6. Trust the pipeline — validation and routing errors are handled automatically with optimal formatting