Skip to content

Without MVA vs With MVA

Prerequisites

Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.


THE AHA MOMENT
See the difference in 3 seconds.
Raw MCP vs Vurb.
Every tool response in raw MCP is JSON.stringify() — the AI gets a flat blob and guesses. Vurb replaces guessing with structured perception.
AspectRaw MCPVurb MVA
Tool count50 individual tools. Token explosion.Action consolidation — module.action discriminator
ResponseJSON.stringify() — AI guessesStructured perception — data + rules + UI + affordances
Domain contextamount_cents: 45000 — dollars? cents?System rules: "amount_cents is in CENTS."
Next actionsAI hallucinates tool namesAgentic HATEOAS — .suggest() based on state
Large datasets10,000 rows dump — token DDoS.limit(50) truncates and teaches filters
SecurityInternal fields leakSchema IS the boundary
Error recoverythrow new Error('not found') — gives uptoolError() with recovery hints
MiddlewareCopy-paste auth checkstRPC-style defineMiddleware()
DeploymentStdio onlyVercel, Cloudflare, Lambda

Invoice: Before & After

RAW MCP
typescript
server.setRequestHandler(async (request) => {
  const { name, arguments: args } = request.params;
  if (name === 'get_invoice') {
    const invoice = await db.invoices.findUnique(args.id);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(invoice)
      }]
    };
  }
  // ...50 more if/else branches
});
// AI: { "internal_margin": 0.12,
//        "customer_ssn": "123-45-6789" } ← leaked
VURB MVA
typescript
const InvoicePresenter = createPresenter('Invoice')
  .schema(InvoiceModel)
  .rules([
    'amount_cents is in CENTS. Divide by 100.',
    'Always show currency as USD.',
  ])
  .ui((inv) => [
    ui.echarts({
      series: [{ type: 'gauge',
        data: [{ value: inv.amount_cents / 100 }]
      }],
    }),
  ])
  .suggest((inv) =>
    inv.status === 'pending'
      ? [suggest('billing.pay', 'Process payment')]
      : [suggest('billing.archive', 'Archive')]
  );

Users: Before & After

RAW MCP — TOKEN DDoS
typescript
case 'list_users':
  const users = await db.users.findMany();
  return { content: [{
    type: 'text',
    text: JSON.stringify(users)
  }]};
  // 10,000 users × ~500 tokens = context DDoS
VURB — SMART TRUNCATION
typescript
const UserPresenter = createPresenter('User')
  .schema(UserModel)
  .limit(50)
  .suggest(() => [
    suggest('users.search',
      'Search by name or role'),
  ]);
// 50 users. Agent guided to filters.
// ~25k tokens instead of ~5,000,000.

Error Recovery: Before & After

RAW MCP — GIVES UP
typescript
if (!invoice) {
  return {
    content: [{
      type: 'text',
      text: 'Invoice not found'
    }],
    isError: true
  };
}
// AI: "I encountered an error." ← dead end
VURB — SELF-HEALS
typescript
if (!invoice) {
  return toolError('NOT_FOUND', {
    message: `Invoice ${args.id} not found`,
    recovery: {
      action: 'list',
      suggestion: 'List invoices first'
    },
    suggestedArgs: { status: 'pending' },
  });
}
// AI: "Let me list pending invoices..." ✓

The Architecture Difference

Without MVA
Handler → JSON.stringify() → raw data blob → LLM guesses everything
With MVA
Handler → raw data → Presenter (Schema + Rules + UI + Limits + Suggestions) → Structured Perception Package → LLM acts with confidence
Without MVAWith MVA
Lines of code per tool20-50 (routing + validation + formatting)3-5 (handler only)
SecurityHope you didn't forget to strip fieldsSchema IS the boundary
Token cost per callHigh (raw dumps)Low (guardrails, truncation)
DeploymentStdio + manual HTTP bridgeStdio, SSE, Vercel, Cloudflare
MaintenanceEvery tool re-implements renderingPresenter defined once