Skip to content

Without MVA vs With MVA

Every MCP server today follows the same pattern: raw JSON output, manual routing, zero guardrails. The table below shows what changes when you adopt MVA.

The Quick Comparison

AspectWithout MVAWith MVA (mcp-fusion)
Tool count50 individual tools registered. LLM sees ALL of them. Token explosion.Action consolidation — 5,000+ operations behind ONE tool via module.action discriminator. 10x fewer tokens.
Response formatRaw JSON.stringify() — the AI parses and guessesStructured perception package — validated data + rules + UI + affordances
Domain contextNone. amount_cents: 45000 — is it dollars? cents? yen?System rules travel with the data: "CRITICAL: amount_cents is in CENTS. Divide by 100."
Next actionsThe AI hallucinates tool namesAgentic HATEOAS.suggestActions() provides explicit hints based on data state
Large datasets10,000 rows dump into context — token DDoSCognitive guardrails.agentLimit(50) truncates and teaches the agent to use filters
SecurityInternal fields (password_hash, ssn) leak to LLMSchema as boundary — Zod .strict() rejects undeclared fields with actionable errors. Automatic.
ReusabilitySame entity rendered differently by different toolsPresenter defined once, reused everywhere. Same rules, same UI, same affordances
Charts & visualsNot possible — text onlyUI Blocks.uiBlocks() renders ECharts, Mermaid diagrams, summaries server-side
Routingswitch/case with hundreds of branchesHierarchical groupsplatform.users.list, platform.billing.refund — infinite nesting
ValidationManual if (!args.id) checksZod schema at the framework level. Handlers receive only valid, typed data
Error recoverythrow new Error('not found') — the AI gives upSelf-healing errorstoolError() with recovery hints and suggested retry args
MiddlewareCopy-paste auth checks in every handlertRPC-styledefineMiddleware() with context derivation, pre-compiled chains
CompositionFlat responses, no nestingPresenter embedding.embed() nests child Presenters. Rules and UI merge automatically
Cache signalsNone — the AI re-fetches stale data foreverState synccacheSignal() and invalidates() — RFC 7234-inspired temporal awareness
Token efficiencyFull JSON payloads every timeTOON encodingtoonSuccess() reduces token count by ~40%
Type safetyManual type casting, no client typesType-safe clientcreateFusionClient() with end-to-end inference, catches errors at build time
StreamingNo progress feedback during long operationsGenerator-based streamingyield progress(0.5, 'Processing...')
Tool exposureAll or nothingTag filtering — selective tool exposure per session with .tags() and filter
ImmutabilityMutable state, runtime surprisesFreeze-after-buildObject.freeze() prevents mutations after build
Observabilityconsole.log()Zero-overhead observercreateDebugObserver() with typed event system

Side-by-Side Code

Returning an invoice

typescript
// ❌ Raw MCP — the AI is on its own
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    if (name === 'get_invoice') {
        const invoice = await db.invoices.findUnique(args.id);
        // Raw JSON. No rules. No hints. No security boundary.
        return {
            content: [{
                type: 'text',
                text: JSON.stringify(invoice)
            }]
        };
    }
    // ...50 more if/else branches
});

// What the AI receives:
// { "id": "inv_123", "amount_cents": 45000, "status": "pending",
//   "internal_margin": 0.12, "customer_ssn": "123-45-6789" }
//
// Problems:
// - AI doesn't know amount_cents is in cents → displays $45,000 instead of $450
// - Internal fields leak (margin, SSN)
// - AI doesn't know it can call "pay" next
// - No visual representation
typescript
// ✅ mcp-fusion — the Presenter handles perception
const InvoicePresenter = createPresenter('Invoice')
    .schema(z.object({
        id: z.string(),
        amount_cents: z.number(),
        status: z.enum(['paid', 'pending', 'overdue']),
        // internal_margin and customer_ssn are NOT in the schema
        // → rejected with actionable error naming each invalid field.
    }))
    .systemRules([
        'CRITICAL: amount_cents is in CENTS. Divide by 100 for display.',
        'Always show currency as USD.',
    ])
    .uiBlocks((inv) => [
        ui.echarts({
            series: [{ type: 'gauge', data: [{ value: inv.amount_cents / 100 }] }]
        }),
    ])
    .suggestActions((inv) =>
        inv.status === 'pending'
            ? [{ tool: 'billing.pay', reason: 'Invoice is pending — process payment' }]
            : [{ tool: 'billing.archive', reason: 'Invoice is settled — archive it' }]
    );

const billing = defineTool<AppContext>('billing', {
    actions: {
        get_invoice: {
            returns: InvoicePresenter, // ← One line. That's it.
            params: { id: 'string' },
            handler: async (ctx, args) => ctx.db.invoices.findUnique(args.id),
        },
    },
});

// What the AI receives:
// ── System Rules ──
// CRITICAL: amount_cents is in CENTS. Divide by 100 for display.
// Always show currency as USD.
//
// ── Data ──
// { "id": "inv_123", "amount_cents": 45000, "status": "pending" }
// (internal_margin and customer_ssn were rejected by .strict())
//
// ── UI ──
// [ECharts gauge: $450.00]
//
// ── Suggested Actions ──
// → billing.pay — "Invoice is pending — process payment"

Listing users with guardrails

typescript
// ❌ Returns ALL 10,000 users into the context window
case 'list_users':
    const users = await db.users.findMany();
    return {
        content: [{
            type: 'text',
            text: JSON.stringify(users) // 10,000 users × 500 tokens each = context DDoS
        }]
    };

// Result: $8.75 per API call (GPT-5.2). Context overflow. Degraded accuracy.
typescript
// ✅ Cognitive guardrails protect the context window
const UserPresenter = createPresenter('User')
    .schema(z.object({ id: z.string(), name: z.string(), role: z.string() }))
    .agentLimit(50, {
        warningMessage: 'Showing {shown} of {total}. Use filters to narrow results.',
    })
    .suggestActions(() => [
        { tool: 'users.search', reason: 'Search by name or role for specific users' },
    ]);

// Result: 50 users shown. Agent guided to use filters.
// Cost: ~$0.04 per call (GPT-5.2). Context protected.

Error recovery

typescript
// ❌ The AI receives "Error" and gives up
if (!invoice) {
    return {
        content: [{ type: 'text', text: 'Invoice not found' }],
        isError: true
    };
}
// AI: "I encountered an error. Please try again."
// (It has no idea what to try differently)
typescript
// ✅ Self-healing errors with recovery hints
if (!invoice) {
    return toolError('NOT_FOUND', {
        message: `Invoice ${args.id} not found`,
        recovery: {
            action: 'list',
            suggestion: 'List invoices to find the correct ID',
        },
        suggestedArgs: { status: 'pending' },
    });
}
// AI: "Invoice not found. Let me list pending invoices to find the right one."
// → Automatically calls billing.list with { status: 'pending' }

The Architecture Difference

text
Without MVA:                          With MVA:
┌──────────┐                          ┌──────────┐
│  Handler  │→ JSON.stringify() →     │  Handler  │→ raw data →
│           │  raw data to LLM        │           │
└──────────┘                          └──────────┘

                                      ┌──────────────────────┐
                                      │     Presenter        │
                                      │ ┌──────────────────┐ │
                                      │ │ Schema (strict)  │ │
                                      │ │ System Rules     │ │
                                      │ │ UI Blocks        │ │
                                      │ │ Agent Limit      │ │
                                      │ │ Suggest Actions  │ │
                                      │ │ Embeds           │ │
                                      │ └──────────────────┘ │
                                      └──────────────────────┘

                                      Structured Perception
                                      Package → LLM

Summary

Without MVAWith MVA
Lines of code per tool20-50 (routing + validation + formatting)3-5 (handler only — framework handles the rest)
SecurityHope you didn't forget to strip fieldsSchema IS the boundary. .strict() rejects. Automatic.
Agent accuracy~60-70% on complex tasks~95%+ with deterministic rules and affordances
Token cost per callHigh (raw dumps, large payloads)Low (guardrails, TOON encoding, truncation)
MaintenanceEvery tool re-implements renderingPresenter defined once, reused across all tools