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: ~5,000,000 tokens per call. 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: ~25,000 tokens per call (200x reduction). 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