Publicado 25 sept 202613 min de lectura
MVA, el patrón Model View Agent: una arquitectura de backend para la era de los agentes de IA
MVC asumía que el consumidor de tu interfaz infería el contexto a partir de la experiencia. Un agente no puede. MVA cambia la View por un Presenter, una capa de percepción que adjunta reglas, guardrails y affordances a los datos en el momento en que un agente los lee. El patrón, el código, la matemática de tokens y cinco casos de uso en los que se paga solo.

Por Renato Marinho
Founder · Vinkius
MVA significa Model, View, Agent, o Modelo, Vista, Agente. Es un patrón de arquitectura para el backend con el que habla un agente de IA, y existe porque el consumidor de tu interfaz cambió de especie.
Durante cincuenta años diseñamos interfaces para un consumidor capaz de inferir. Un humano lee amount_cents: 45000 y sabe que son cuatrocientos cincuenta dólares, porque ya vio facturas antes. Un agente lee el mismo campo y tiene que adivinar. A veces adivina cuarenta y cinco mil dólares. A veces inventa una herramienta de pago que no existe, o se salta un flujo obligatorio porque nadie le avisó que el flujo estaba ahí. Ninguno de esos es un problema de prompt. Todos son problemas de arquitectura.
Yo nombré y escribí el patrón mientras construía MCP Fusion, el framework TypeScript para servidores MCP en producción, donde MVA es la forma por defecto de construir una herramienta. Esta es la explicación que le daría a un ingeniero senior que me preguntara qué es MVA, por qué no es un MVC renombrado, y dónde realmente se paga solo.
MVA en un párrafo
MVA reemplaza la View orientada a humanos de MVC por un Presenter: una capa de percepción que se ubica entre tus datos de dominio y el modelo de lenguaje y decide qué ve el agente, cómo debe interpretarlo, y qué puede hacer a continuación. El Model sigue definiendo tus datos de dominio y sigue validándolos. El Presenter envuelve esos datos en un límite de schema, reglas de interpretación, bloques visuales renderizados en el servidor, y acciones siguientes explícitas. El Agent, cualquier LLM, recibe un paquete de percepción estructurado en lugar de JSON crudo, así que deja de adivinar y pasa a seguir instrucciones que viajan con los datos.
| Capa | Qué es | Responsabilidad |
|---|---|---|
| Model | defineModel() | Datos de dominio: tipos de campo, defaults, qué campos se ocultan al agente, cuáles están protegidos de la asignación masiva |
| View | El Presenter | Percepción: límite de schema, reglas, bloques de UI, límites de truncamiento, acciones siguientes, redacción de PII |
| Agent | Cualquier LLM | Consume el paquete, razona, llama a la siguiente herramienta de la lista ofrecida |
Por qué MVC deja de funcionar
MVC asumía un consumidor que renderiza interfaz visual, tiene noción espacial, elige entre opciones visibles, y no alucina el siguiente paso. Un agente no tiene ninguna de esas propiedades, y cuatro modos de fallo estructurales aparecen en el momento en que apuntas un LLM a una API cruda.
Inanición de contexto. Los datos llegan sin reglas de interpretación. { amount_cents: 45000 } no tiene ninguna regla adjunta, así que el modelo se apoya en el nombre del campo, y la convención de nombres no es una señal confiable. He visto el mismo campo renderizarse como dólares, euros y "procesando" en tres conversaciones distintas.
Ceguera de acción. Después de leer los datos, el agente debe decidir qué hacer a continuación. Sin affordances inventa nombres de herramientas, llama a billing.process_payment cuando la herramienta es billing.pay, o simplemente se detiene porque no sabía que existía una acción.
Deriva de percepción. La misma factura vuelve moldeada de forma distinta por dos herramientas, una diciendo amount en dólares y state: "open", la otra diciendo amount_cents y status: "pending". El modelo no puede reconciliarlas como una sola entidad y empieza a comportarse de forma inconsistente dentro de tu propio producto.
Fuga de seguridad. El JSON crudo lleva cada columna que la consulta seleccionó: internal_margin, tenant_id, password_hash. Todo eso entra a la ventana de contexto y puede aparecer en la respuesta que lee el usuario.
Cada uno es un déficit de arquitectura. La ingeniería de prompts no arregla un límite de salida ausente, porque el prompt no es el lugar donde el límite viviría.
MVA en código: las tres capas
El Model declara la entidad de dominio una vez. Los campos que no declaras aquí no pueden llegar al agente.
import { defineModel } from '@mcpfusion/core';
export const InvoiceModel = defineModel('Invoice', m => {
m.casts({
id: m.string('Invoice identifier'),
amount_cents: m.number('Amount in cents. Divide by 100 to display.'),
status: m.enum('Payment status', ['paid', 'pending', 'overdue']),
});
m.hidden(['tenant_id']);
m.guarded(['id']);
m.fillable({
create: ['amount_cents', 'status'],
update: ['status'],
});
});
El Presenter es la View. Se define a nivel de dominio, no por herramienta, así que un InvoicePresenter sirve a cualquier herramienta que devuelva una factura.
import { createPresenter, ui, suggest } from '@mcpfusion/core';
export const InvoicePresenter = createPresenter('Invoice')
.schema(InvoiceModel)
.rules((invoice, ctx) => [
'CRITICAL: amount_cents is in CENTS. Divide by 100 before display.',
ctx?.user?.role !== 'admin'
? 'RESTRICTED: Mask exact totals for non admin viewers.'
: null,
invoice.status === 'overdue'
? 'WARNING: This invoice is overdue. Mention urgency proactively.'
: null,
])
.ui((invoice) => [
ui.summary(`Invoice ${invoice.id} · ${invoice.status}`),
ui.echarts({ series: [{ type: 'gauge', data: [{ value: invoice.amount_cents / 100 }] }] }),
])
.agentLimit(50, (omitted) =>
ui.summary(`50 shown, ${omitted} hidden. Filter by status or date range.`),
)
.suggest((invoice) => [
suggest('billing.pay', 'Process immediate payment'),
invoice.status === 'overdue'
? suggest('billing.escalate', 'Escalate to collections')
: null,
].filter(Boolean));
La herramienta es la superficie del Agent. El handler devuelve datos crudos; .returns() es donde se adjunta la capa de percepción.
import { initMCPFusion } from '@mcpfusion/core';
const f = initMCPFusion<AppContext>();
export const getInvoice = f.query('billing.get_invoice')
.describe('Get an invoice by ID')
.withString('invoice_id', 'The exact invoice ID')
.returns(InvoicePresenter)
.handle(async (input, ctx) => {
return ctx.db.invoices.findUnique({
where: { id: input.invoice_id },
include: { client: true },
});
});
export const createInvoice = f.action('billing.create')
.describe('Create an invoice')
.fromModel(InvoiceModel, 'create')
.returns(InvoicePresenter)
.handle(async (input, ctx) => ctx.db.invoices.create({ data: input }));
.fromModel(InvoiceModel, 'create') deriva los parámetros de entrada del perfil fillable del modelo, así que el schema de entrada y el schema de salida no pueden divergir. Una declaración, dos límites, ambos aplicados en tiempo de compilación.
Qué recibe realmente el agente
Cuando el handler retorna, el pipeline compone un paquete de percepción estructurado: hasta seis bloques de contenido, en un orden fijo.
Block 1 DATA {"id":"INV-001","amount_cents":45000,"status":"pending"}
(tenant_id and internal_margin rejected by the schema boundary)
Block 2 UI [echarts gauge: 450.00] with a pass through directive:
"send this block to the interface, do not redraw it"
Block 3 EMBEDS rules and blocks from ClientPresenter, merged by .embed()
Block 4 HINTS situational notes added by the handler or middleware
Block 5 RULES [DOMAIN RULES] CRITICAL: amount_cents is in CENTS...
Block 6 ACTIONS → billing.pay: Process immediate payment
→ billing.escalate: Escalate to collections
El orden no es cosmético. Los modelos de lenguaje ponderan más el final del contexto, así que las reglas de interpretación y las acciones disponibles llegan al final, justo donde el modelo está a punto de decidir qué llamar. Los datos van primero, porque fundamentan la interpretación que sigue.
El contraste con lo que devuelve un servidor MCP crudo es el argumento completo en una pantalla:
// raw SDK: one text block, JSON.stringify, no boundary
return {
content: [{ type: 'text', text: JSON.stringify(invoice) }],
};
// the agent receives all columns, zero rules, zero next actions
Cinco casos de uso donde MVA se paga solo
1. Facturación y fintech. El ejemplo de la factura de arriba no es decorativo. El dinero es el dominio donde una suposición errónea es un ticket de soporte o un incidente de cumplimiento. La regla de los centavos, la regla de enmascaramiento por rol, y la affordance de pago son tres líneas en un Presenter, y todas las herramientas de facturación las heredan. Un desarrollador junior no puede publicar una herramienta de facturación que olvide la regla de dividir entre cien, porque la regla no está en su herramienta.
2. Operaciones a escala. Una llamada tasks.list contra una tabla sin límite devuelve tres mil filas a aproximadamente quinientos tokens cada una, lo que da cerca de 1,5 millones de tokens en una ventana de contexto que no los puede contener. .agentLimit(50, ...) trunca el arreglo y añade un bloque didáctico que nombra los parámetros de filtro reales: status, assignee, sprint_id, due_before. La siguiente llamada del modelo es una consulta filtrada. Solo truncar le daría la misma página truncada para siempre; truncar más enseñar crea un bucle de autocorrección.
3. Datos regulados y personales. Salud, RRHH y cualquier CRM con PII necesitan un límite más fuerte que "por favor no muestres esto". redactPII compila rutas de objeto como patients[*].diagnosis en funciones de enmascaramiento, así que el valor enmascarado llega al modelo mientras los bloques de UI y las reglas siguen viendo los datos reales. Combinado con m.hidden(), los campos que nunca pueden salir de la base de datos son los campos que nunca fueron declarados.
4. SaaS multi tenant. Las reglas en el Presenter reciben el contexto de la petición, así que la misma factura se percibe de forma distinta por un admin y por un rol de visualización, y las fechas se formatean en el locale del tenant sin una segunda vía de código. Esto reemplaza una clase de ramificación de prompt por tenant que los equipos suelen mantener a mano y suelen hacer mal.
5. Flujos de aprobación y pedidos. Las affordances se calculan a partir del estado actual, no de una lista estática. Un pedido pendiente sugiere orders.approve; un pedido ya enviado no. Esto es HATEOAS para herramientas: se le dice al modelo qué transiciones son legales desde el estado en el que realmente está, lo que elimina la clase más común de llamada alucinada.
Hay un sexto caso que vale la pena nombrar porque es en el que está la mayoría de los equipos. Ya tienes un dashboard en React, una API REST, y una base de datos. MVA no te pide que los reemplaces. Mantienes MVC para humanos, añades una superficie orientada a agentes, y ambos consumen el mismo modelo de dominio. Ese patrón de interfaz dual es donde aterriza la mayor parte del trabajo "AI nativo", y nombrarlo convierte la migración en un proyecto acotado en lugar de una reescritura.
La matemática de tokens
El otro lugar donde MVA se paga es la factura. La corrección convencional para una capa de percepción ausente es atiborrar de reglas de dominio el prompt de sistema global, que se envía en cada llamada, esté o no activo el dominio. Un producto con quince entidades de dominio termina con cerca de dos mil tokens de reglas viajando en cada petición, y el ochenta y siete por ciento de ellas son irrelevantes en cualquier turno.
Context tree shaking es el nombre de la alternativa: las reglas se adjuntan al Presenter y aparecen solo cuando ese dominio está activo. Una conversación de diez turnos pasa de cerca de veinte mil tokens en reglas a cerca de dos mil, y las reglas que llegan son las correctas. El efecto secundario importa tanto como el costo: cuando las reglas de facturación están ausentes de una conversación sobre tareas, el modelo no puede aplicar mal una regla de dividir entre cien a conteos de tareas. He visto ese error exacto en producción, y desaparece cuando la regla no está en el contexto desde el principio.
MVA versus MVC, REST, GraphQL y RPC
| Consumidor | Salida | Acciones siguientes | Límite | |
|---|---|---|---|---|
| MVC | Navegador humano | HTML por página | Links y botones | Template de vista |
| REST | Código cliente | JSON crudo | Los links HATEOAS son URLs | Ninguno |
| GraphQL | Código cliente | Campos elegidos por el cliente | Ninguna | Solo nivel de consulta |
| RPC | Código cliente | Datos tipados crudos | Ninguna | Solo entrada |
| MVA | Agente de IA | Paquete de percepción | Nombres de herramienta a partir del estado | Entrada y salida |
REST con HATEOAS es el ancestro más cercano y merece la distinción más clara. HATEOAS embebe links en la respuesta, que es el instinto correcto, pero un link es una URL a un endpoint HTTP y un agente llama a herramientas por nombre. Las affordances de MVA devuelven nombres de herramienta con razones semánticas, calculadas a partir del estado actual de los datos, junto con reglas y bloques visuales que REST no tiene dónde cargar. GraphQL resuelve qué campos buscar, que nunca fue el cuello de botella; el cuello de botella es cómo interpretar los campos después de buscarlos.
Lo que MVA no es
No es un reemplazo de MVC. Si tu consumidor es un humano con un navegador, MVC y MVVM siguen siendo correctos, y ninguna parte de MVA mejora un dashboard. Tampoco es un framework de agentes: no planifica, no gestiona memoria, no elige modelo. Es el contrato entre tus datos y cualquier modelo que los lea, y vuelve ese contrato explícito, validado y testeable.
Tampoco es todo o nada. Puedes adjuntar un Presenter a una herramienta de un servidor legacy esta tarde y dejar las otras cincuenta en paz.
Cómo empezar
npm install @mcpfusion/core @modelcontextprotocol/sdk
npx mcpfusion create
Crea un proyecto, define un model, define un Presenter, pon .returns() en una query. La prueba más rápida de si el patrón está haciendo algo es leer el resultado de la herramienta antes y después: JSON crudo, y luego el paquete de seis bloques. La diferencia es el argumento.
Si quieres el recorrido completo de un conector construido así, el recorte MVA, el Presenter, y los errores auto reparables se explican con código fuente en Cómo construir tu propio conector MCP. Si quieres el lado del runtime, cómo el tráfico de agentes se mantiene seguro a escala, eso está en Cómo ejecutar agentes de IA en producción con seguridad.
Preguntas frecuentes
¿MVA es solo para MCP? El patrón es independiente del protocolo, y MCP Fusion es su implementación de referencia. Cualquier cosa que ponga un LLM en un extremo de una llamada a una herramienta se beneficia de una capa de percepción. MCP es simplemente donde el límite se hace más visible, porque los servidores MCP entregan JSON crudo por defecto.
¿MVA reemplaza a MVC? No, y el patrón de interfaz dual es el encuadre honesto. Mantén MVC para el dashboard humano, añade MVA para la superficie del agente, comparte el modelo de dominio entre ambos.
¿Qué es un Presenter? Un objeto a nivel de dominio que convierte datos crudos en percepción del agente: un schema, reglas, bloques de UI, una política de truncamiento, acciones siguientes, y rutas de redacción. Uno por entidad, compartido por todas las herramientas que devuelven esa entidad.
¿Cómo reduce MVA la alucinación? Eliminando las situaciones en que alucinar es el camino más barato. Cuando las acciones siguientes son explícitas, el modelo no inventa nombres de herramienta. Cuando el schema es estricto, los campos alucinados se rechazan con la lista de los válidos. Cuando los errores llevan pistas de recuperación, el modelo reintenta bien en lugar de entrar en bucle.
¿Funciona con mi modelo? Sí, si el modelo puede llamar a herramientas. Claude, GPT, Gemini y cualquier cliente compatible con MCP leen el paquete de la misma forma, porque la capa de percepción vive en el servidor.
Por qué lo construí
Cada cambio de arquitectura que he vivido tuvo el mismo disparador: el consumidor cambió, y la arquitectura vieja asumía un consumidor que ya no existía. El navegador se lo hizo al desktop. El móvil se lo hizo al navegador. El agente lo está haciendo ahora, y el supuesto que se rompió es uno que nunca escribimos: que el consumidor llenaría los vacíos.
MVA es el nombre para cerrar los vacíos en la interfaz en lugar de esperar que el modelo los cierre. Escribe el límite, escribe las reglas, escribe las acciones, y deja que el modelo haga lo que realmente se le da bien.
