MCP Fusion/Core concepts/Models et Presenters
Models et Presenters
Déclarez votre domaine une seule fois avec defineModel et façonnez ce que l’agent perçoit avec createPresenter : schemas, rules, masquage, blocs UI, affordances et limites.
Models et Presenters sont le M et le V de the MVA pattern. Le Model déclare votre domaine une seule fois. Le Presenter façonne la perception de ce domaine pour l’agent. La séparation est volontaire : un même Model peut avoir plusieurs Presenters, un par audience ou par tâche.
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(): conversions de type appliquées à la lecture.hidden(): champs qui ne quittent jamais le serveur, même dans la sortie de debug.guarded(): champs que l’agent ne peut pas écrire.fillable(): les seuls champs que chaque opération peut écrire
Tout ce que vous ne déclarez pas n’existe tout simplement pas pour l’agent. Le model compile vers un schema Zod, ainsi InvoiceModel.schema valide les entrées et typeof InvoiceModel.infer vous donne le type TypeScript.
Les annotations .describe() sur les champs sont extraites automatiquement et deviennent des règles système pour l’agent : le sens du champ voyage avec le champ.
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);Les méthodes du builder
| Méthode | Ce qu’elle fait |
|---|---|
.schema() | Les champs qui existent pour l’agent, avec les types t et les descriptions |
.rules() | Lignes de prompt système injectées dans chaque réponse de ce Presenter |
.redactPII() | Chemins de champs retirés avant la sérialisation, le pare-feu de sortie |
.ui() | Blocs rendus comme des tables ; leurs données contournent le masquage par conception |
.suggest() | Affordances : actions suivantes que l’agent peut envisager |
.limit() | Plafond d’éléments renvoyés, avec un message de troncature auto-réparant |
.embed() | Presenters imbriqués pour les entités liées |
Les rules sont des contrats, pas des commentaires
Les lignes de .rules() sont injectées dans le contexte système, donc le modèle ne peut pas les ignorer comme il ignore un commentaire de code. C’est le bon endroit pour les unités, les devises, les statuts et tout ce que le modèle comprend mal en permanence.
Le masquage s’exécute tard, volontairement
Le moteur DLP compile les chemins de .redactPII() en une fonction de masquage optimisée, mise en cache par Presenter. Le masquage s’exécute après le rendu des blocs UI et juste avant la sérialisation : les dashboards conservent les valeurs réelles pendant que le contexte du modèle reste propre.
Les données de .ui() contournent le masquage par conception : une table destinée à un opérateur humain peut afficher des valeurs réelles. Gardez les secrets hors des blocs .ui(), sauf si un humain est le seul consommateur.
Une entité, plusieurs audiences
const AdminPresenter = createPresenter('Invoice').schema({ /* everything */ });
const SupportPresenter = createPresenter('Invoice')
.schema({ id: t.string, status: t.enum('draft', 'paid', 'overdue') })
.limit(10);La perception par rôle ne demande aucun module supplémentaire : choisissez le Presenter par tenant ou par rôle dans le middleware, et la même tool sert les deux audiences en toute sécurité. Voir Credentials et Governance.
