MCP Fusion/Core concepts/Models and Presenters
Models and Presenters
Declare your domain once with defineModel, shape what the agent perceives with createPresenter: schemas, rules, redaction, UI blocks, affordances and limits.
Models and Presenters are the M and the V of the MVA pattern. The Model declares your domain once. The Presenter shapes the perception of that domain for the agent. They are separate on purpose: the same Model may have several Presenters, one per audience or task.
Models
import { defineModel } from '@mcpfusion/core';
const InvoiceModel = defineModel('Invoice', (m) => {
m.casts({ amount_cents: 'number' });
m.hidden(['internal_notes', 'cost_basis']);
m.guarded(['id', 'created_at']);
m.fillable({
create: ['customer_id', 'amount_cents', 'due_date'],
update: ['status'],
});
});.casts(): type coercions applied on read.hidden(): fields that never leave the server, even in debug output.guarded(): fields the agent cannot write.fillable(): the only fields each operation may write
Anything you do not declare simply does not exist for the agent. The model compiles to a Zod schema, so InvoiceModel.schema validates inputs and typeof InvoiceModel.infer gives you the TypeScript type.
.describe() annotations on fields are extracted automatically and become system rules for the agent, so field meaning travels with the field.
Presenters
import { createPresenter, t, suggest, ui } from '@mcpfusion/core';
const InvoicePresenter = createPresenter('Invoice')
.schema({
id: t.string,
customer: t.string,
amount_cents: t.number.describe('CENTS, divide by 100'),
status: t.enum('draft', 'paid', 'overdue'),
})
.rules([
'CRITICAL: amount_cents is in CENTS. Divide by 100.',
])
.redactPII(['customer.email', 'customer.ssn'])
.ui((inv) => [
ui.table(['Field', 'Value'], [
['Invoice', inv.id],
['Amount', inv.amount_cents / 100],
['Status', inv.status],
]),
])
.suggest((inv) => [
inv.status === 'overdue'
? suggest('billing.remind', 'Send a payment reminder')
: null,
])
.limit(50);The builder methods
| Method | What it does |
|---|---|
.schema() | The fields that exist for the agent, with t types and descriptions |
.rules() | System prompt lines injected with every response of this Presenter |
.redactPII() | Field paths removed before serialization, the egress firewall |
.ui() | Rendered blocks such as tables; their data bypasses redaction by design |
.suggest() | Affordances: next actions the agent may consider |
.limit() | Cap on items returned, with a self healing truncation message |
.embed() | Nested presenters for related entities |
Rules are contracts, not comments
.rules() lines are injected into the system context, so the model cannot ignore them the way it ignores a code comment. They are the right place for units, currencies, statuses and anything the model keeps getting wrong.
Redaction runs late, on purpose
The DLP engine compiles .redactPII() paths into an optimized redaction function cached per Presenter. Redaction runs after UI blocks render and right before serialization, so dashboards keep real values while the model context stays clean.
.ui() data bypasses redaction by design: a table meant for a human operator may show real values. Keep secrets out of .ui() blocks unless a human is the only consumer.
One entity, many audiences
const AdminPresenter = createPresenter('Invoice').schema({ /* everything */ });
const SupportPresenter = createPresenter('Invoice')
.schema({ id: t.string, status: t.enum('draft', 'paid', 'overdue') })
.limit(10);Role based perception needs no extra module: pick the Presenter per tenant or role in middleware, and the same tool serves both audiences safely. See Credentials and Governance.
