Presenter
Prerequisites
Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.
The V in MVA (Model-View-Agent). Define InvoicePresenter once — every tool and prompt that touches invoices uses the same schema, rules, and affordances. Internal fields never leak. Token usage stays under control.
Defining a Presenter
That's it. Any field not in the schema is stripped from the response. The agent never sees passwordHash, tenantId, or anything else you didn't declare.
Declarative alternative
definePresenter({ name: 'User', schema: UserModel }) — same result, config-object style.
Schema — The t Namespace
The t namespace provides Zod-backed type helpers. Every t.* IS a real ZodType — .describe(), .optional(), .nullable() all work.
| Helper | Zod Equivalent | Example |
|---|---|---|
t.string | z.string() | t.string.describe('User ID') |
t.number | z.number() | t.number |
t.boolean | z.boolean() | t.boolean |
t.date | z.date() | t.date |
t.enum(...) | z.enum([...]) | t.enum('active', 'archived') |
t.array(T) | z.array(T) | t.array(t.string) |
t.object({}) | z.object({}) | t.object({ lat: t.number }) |
t.record(T) | z.record(T) | t.record(t.string) |
t.optional(T) | T.optional() | t.optional(t.string) |
t.nullable(T) | T.nullable() | t.nullable(t.string) |
t.zod | z | t.zod.string().email() |
Escape Hatch
Need regex, transforms, or unions? Use t.zod for direct Zod access — full power, zero limits.
System Rules
Zod .describe() annotations auto-generate rules. null values are filtered. Static and dynamic rules merge.
UI Blocks
| Helper | What it renders |
|---|---|
ui.echarts({...}) | Interactive charts |
ui.mermaid('graph TD; A-->B') | Diagrams |
ui.markdown('**Bold** text') | Rich text |
ui.table(['ID', 'Amount'], rows) | Markdown tables |
ui.summary('3 invoices found.') | Collection summaries |
ui.json({ key: 'value' }) | Formatted JSON |
For arrays, use .collectionUiBlocks() to get aggregate visualizations instead of N individual charts.
Agent Limit
Slices arrays before validation. The agent receives kept items plus a UI block telling it how to narrow results.
Suggested Actions
The agent receives valid next actions with reasons — no need to scan the full tools/list.
Embeds — Nested Presenters
Rules, UI blocks, and affordances from children merge into the parent. Embeds nest to any depth.
Tool Integration
The handler's only job is to query data. The framework calls presenter.make(data, ctx).build() automatically.
Execution Pipeline
Every stage is optional. A Presenter with only name and schema is a pure egress whitelist.
.parse() — undeclared fields are stripped, types are validated. Internal data never leaks..describe() + static rules + dynamic context-aware rules — all merged, nulls filtered..ui() or aggregate .collectionUiBlocks() — charts, tables, summaries.Builder API
Both shorthand aliases and full method names work:
| Shorthand | Full control | Purpose |
|---|---|---|
.schema({ id: t.string }) | .schema(InvoiceModel) | Validation schema |
.rules([...]) | .systemRules([...]) | JIT system rules |
.ui((item) => [...]) | .uiBlocks((item) => [...]) | Per-item UI blocks |
.limit(50) | .agentLimit(50, onTruncate) | Cognitive guardrail |
.suggest((item) => [...]) | .suggestActions((item) => {...}) | HATEOAS suggestions |
After the first .make() call, the Presenter is sealed — configuration methods throw if called.
Prompt Integration
PromptMessage.fromView() decomposes a Presenter's output into prompt messages — same schema, same rules, in both tools and prompts:
Error Handling
When validation fails, a PresenterValidationError is thrown with per-field details:
import { PresenterValidationError } from '@vurb/core';
try {
InvoicePresenter.make(badData);
} catch (err) {
if (err instanceof PresenterValidationError) {
console.error(err.presenterName); // 'Invoice'
console.error(err.cause); // Original ZodError
}
}