Site
Todos os artigos

Publicado 25 de set. de 202613 min de leitura

MVA, o padrão Model View Agent: uma arquitetura de backend para a era dos agentes de IA

O MVC presumia que o consumidor da sua interface inferisse o contexto a partir da experiência. Um agente não consegue. O MVA troca a View por um Presenter, uma camada de percepção que anexa regras, guardrails e affordances aos dados no momento em que um agente os lê. O padrão, o código, a matemática de tokens e cinco casos de uso em que ele se paga.

Renato Marinho

Por Renato Marinho

Founder · Vinkius

The MVA pattern: a raw database row crosses the Model boundary where undeclared fields are dropped, the Presenter wraps the surviving fields into a six block perception package of data, ui, embeds, hints, rules and actions, and the agent reads that package and picks the next tool from the offered affordances

MVA significa Model, View, Agent, ou Modelo, Visão, Agente. É um padrão de arquitetura para o backend com o qual um agente de IA se comunica, e ele existe porque o consumidor da sua interface mudou de espécie.

Durante cinquenta anos projetamos interfaces para um consumidor capaz de inferir. Um humano lê amount_cents: 45000 e sabe que são quatrocentos e cinquenta dólares, porque já viu faturas antes. Um agente lê o mesmo campo e precisa adivinhar. Às vezes adivinha quarenta e cinco mil dólares. Às vezes inventa uma ferramenta de pagamento que não existe, ou pula um fluxo obrigatório porque ninguém avisou que ele estava lá. Nenhum desses é um problema de prompt. Todos são problemas de arquitetura.

Eu nomeei e escrevi o padrão enquanto construía o MCP Fusion, o framework TypeScript para servidores MCP em produção, onde MVA é a forma padrão de construir uma ferramenta. Esta é a explicação que eu daria a um engenheiro sênior que me perguntasse o que é MVA, por que não é um MVC renomeado, e onde ele realmente se paga.

MVA em um parágrafo

MVA substitui a View voltada para humanos do MVC por um Presenter: uma camada de percepção que fica entre os seus dados de domínio e o modelo de linguagem e decide o que o agente vê, como ele deve interpretar isso, e o que pode fazer a seguir. O Model ainda define os dados de domínio e ainda os valida. O Presenter envolve esses dados em um limite de schema, regras de interpretação, blocos visuais renderizados no servidor, e ações seguintes explícitas. O Agent, qualquer LLM, recebe um pacote de percepção estruturado em vez de JSON cru, então para de adivinhar e passa a seguir instruções que viajam com os dados.

CamadaO que éResponsabilidade
ModeldefineModel()Dados de domínio: tipos de campo, defaults, quais campos são ocultados do agente, quais são protegidos contra atribuição em massa
ViewO PresenterPercepção: limite de schema, regras, blocos de UI, limites de truncamento, ações seguintes, redação de PII
AgentQualquer LLMConsome o pacote, raciocina, chama a próxima ferramenta da lista oferecida

Por que o MVC para de funcionar

O MVC assumia um consumidor que renderiza interface visual, tem noção espacial, escolhe entre opções visíveis, e não alucina o próximo passo. Um agente não tem nenhuma dessas propriedades, e quatro modos de falha estruturais aparecem no momento em que você aponta um LLM para uma API crua.

Fome de contexto. Os dados chegam sem regras de interpretação. { amount_cents: 45000 } não tem regra anexada, então o modelo se apoia no nome do campo, e convenção de nome de campo não é um sinal confiável. Eu vi o mesmo campo ser renderizado como dólares, euros e "processando" em três conversas diferentes.

Cegueira de ação. Depois de ler os dados, o agente precisa decidir o que fazer a seguir. Sem affordances ele inventa nomes de ferramenta, chama billing.process_payment quando a ferramenta é billing.pay, ou simplesmente para porque não sabia que uma ação existia.

Divergência de percepção. A mesma fatura volta moldada de forma diferente por duas ferramentas, uma dizendo amount em dólares e state: "open", a outra dizendo amount_cents e status: "pending". O modelo não consegue reconciliar as duas como uma entidade só e passa a se comportar de forma inconsistente dentro do seu próprio produto.

Vazamento de segurança. JSON cru carrega toda coluna que a consulta selecionou: internal_margin, tenant_id, password_hash. Tudo isso entra na janela de contexto e pode aparecer na resposta que o usuário lê.

Cada um desses é um déficit de arquitetura. Engenharia de prompt não conserta um limite de saída ausente, porque o prompt não é o lugar onde o limite viveria.

MVA em código: as três camadas

O Model declara a entidade de domínio uma vez. Campos que você não declara aqui não chegam ao 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'],
  });
});

O Presenter é a View. É definido no nível de domínio, não por ferramenta, então um InvoicePresenter atende a qualquer ferramenta que devolva uma fatura.

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));

A ferramenta é a superfície do Agent. O handler devolve dados crus; .returns() é onde a camada de percepção é anexada.

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 os parâmetros de entrada do perfil fillable do modelo, então o schema de entrada e o schema de saída não conseguem divergir. Uma declaração, dois limites, ambos aplicados em tempo de compilação.

O que o agente realmente recebe

Quando o handler retorna, o pipeline compõe um pacote de percepção estruturado: até seis blocos de conteúdo, em uma ordem fixa.

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

A ordem não é cosmética. Modelos de linguagem ponderam mais o final do contexto, então as regras de interpretação e as ações disponíveis são as últimas a chegar, exatamente onde o modelo está prestes a decidir o que chamar. Os dados vêm primeiro, porque eles fundamentam a interpretação que se segue.

O contraste com o que um servidor MCP cru devolve é o argumento inteiro em uma tela:

// 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 onde MVA se paga

1. Cobrança e fintech. O exemplo da fatura acima não é decorativo. Dinheiro é o domínio onde um palpite errado é um ticket de suporte ou um incidente de compliance. A regra dos centavos, a regra de mascaramento por papel, e a affordance de pagamento são três linhas em um Presenter, e todas as ferramentas de cobrança as herdam. Um desenvolvedor júnior não consegue publicar uma ferramenta de cobrança que esquece a regra de dividir por cem, porque a regra não está na ferramenta dele.

2. Operações em escala. Uma chamada tasks.list contra uma tabela sem limite devolve três mil linhas a cerca de quinhentos tokens cada, o que dá cerca de 1,5 milhão de tokens em uma janela de contexto que não comporta isso. .agentLimit(50, ...) trunca o array e acrescenta um bloco didático que nomeia os parâmetros de filtro reais: status, assignee, sprint_id, due_before. A próxima chamada do modelo é uma consulta filtrada. Só truncar faria ele ler a mesma página truncada para sempre; truncar e ensinar cria um loop de autocorreção.

3. Dados regulados e pessoais. Saúde, RH e qualquer CRM com PII precisam de um limite mais forte que "por favor não mostre isso". redactPII compila camadas de objeto como patients[*].diagnosis em funções de mascaramento, então o valor mascarado chega ao modelo enquanto os blocos de UI e as regras ainda veem os dados reais. Combinado com m.hidden(), os campos que nunca podem sair do banco são os campos que nunca foram declarados.

4. SaaS multi tenant. As regras no Presenter recebem o contexto da requisição, então a mesma fatura é percebida de forma diferente por um admin e por um papel de visualização, e as datas são formatadas no locale do tenant sem uma segunda codificação. Isso substitui uma classe de ramificação de prompt por tenant que times costumam manter à mão e costumam fazer errado.

5. Fluxos de aprovação e pedido. As affordances são calculadas a partir do estado atual, não de uma lista estática. Um pedido pendente sugere orders.approve; um pedido já enviado não sugere. Isso é HATEOAS para ferramentas: o modelo é informado das transições válidas a partir do estado em que ele realmente está, o que remove a classe mais comum de chamada alucinada.

Existe um sexto caso que vale nomear porque é onde a maioria dos times está. Você já tem um dashboard em React, uma API REST, e um banco. MVA não pede que você substitua eles. Você mantém MVC para humanos, adiciona uma superfície voltada para agentes, e ambos consomem o mesmo modelo de domínio. Esse padrão de interface dupla é onde a maior parte do trabalho "AI nativo" realmente aterrissa, e dar nome a isso transforma a migração em um projeto delimitado em vez de uma reescrita.

A matemática de tokens

O outro lugar onde MVA se paga é a fatura. A correção convencional para uma camada de percepção ausente é enfiar regras de domínio no prompt de sistema global, que é enviado em cada chamada, esteja o domínio ativo ou não. Um produto com quinze entidades de domínio termina com cerca de dois mil tokens de regras viajando em cada requisição, e oitenta e sete por cento delas são irrelevantes em qualquer turno.

Context tree shaking: um prompt de sistema global envia cerca de dois mil tokens de regras em cada um de dez turnos, cerca de vinte mil no total e oitenta e sete por cento irrelevantes; regras anexadas ao presenter enviam só o domínio ativo, cerca de duzentos tokens por turno, dois mil no total, uma economia de cerca de dezoito mil tokens por conversa

Context tree shaking é o nome da alternativa: as regras são anexadas ao Presenter e aparecem apenas quando aquele domínio está ativo. Uma conversa de dez turnos passa de cerca de vinte mil tokens em regras para cerca de dois mil, e as regras que chegam são as corretas. O efeito colateral importa tanto quanto o custo: quando as regras de fatura estão ausentes de uma conversa sobre tarefas, o modelo não consegue aplicar errado uma regra de divisão por cem em contagens de tarefa. Eu vi esse erro exato em produção, e ele some quando a regra não está no contexto para começar.

MVA versus MVC, REST, GraphQL e RPC

ConsumidorSaídaPróximas açõesLimite
MVCNavegador humanoHTML por páginaLinks e botõesTemplate de view
RESTCódigo clienteJSON cruLinks HATEOAS são URLsNenhum
GraphQLCódigo clienteCampos escolhidos pelo clienteNenhumaApenas nível de consulta
RPCCódigo clienteDados tipados crusNenhumaApenas entrada
MVAAgente de IAPacote de percepçãoNomes de ferramenta a partir do estadoEntrada e saída

REST com HATEOAS é o ancestral mais próximo e merece a distinção mais clara. HATEOAS embute links na resposta, que é o instinto certo, mas um link é uma URL para um endpoint HTTP e um agente chama ferramentas por nome. As affordances de MVA devolvem nomes de ferramenta com razões semânticas, calculadas a partir do estado atual dos dados, junto com regras e blocos visuais que o REST não tem onde carregar. GraphQL resolve quais campos buscar, que nunca foi o gargalo; o gargalo é como interpretar os campos depois de buscar.

O que MVA não é

Não é uma substituição do MVC. Se o seu consumidor é um humano com um navegador, MVC e MVVM continuam corretos, e nenhuma parte de MVA melhora um dashboard. Também não é um framework de agentes: ele não planeja, não gerencia memória, não escolhe modelo. Ele é o contrato entre os seus dados e qualquer modelo que os leia, e torna esse contrato explícito, validado e testável.

Também não é tudo ou nada. Você pode anexar um Presenter a uma ferramenta em um servidor legado nesta tarde e deixar as outras cinquenta em paz.

Como começar

npm install @mcpfusion/core @modelcontextprotocol/sdk
npx mcpfusion create

Crie um projeto, defina um model, defina um Presenter, coloque .returns() em uma query. O teste mais rápido de se o padrão está fazendo algo é ler o resultado da ferramenta antes e depois: JSON cru, e em seguida o pacote de seis blocos. A diferença é o argumento.

Se você quiser o passo a passo completo de um conector construído dessa forma, o recorte MVA, o Presenter, e os erros auto reparáveis estão explicados com código fonte em Como construir seu próprio conector MCP. Se você quiser o lado do runtime, como o tráfego de agentes se mantém seguro em escala, isso está em Como rodar agentes de IA em produção com segurança.

Perguntas frequentes

MVA serve só para MCP? O padrão é independente de protocolo, e MCP Fusion é a sua implementação de referência. Qualquer coisa que coloque um LLM em uma ponta de uma chamada de ferramenta se beneficia de uma camada de percepção. MCP é simplesmente onde o limite fica mais visível, porque servidores MCP entregam JSON cru por padrão.

MVA substitui o MVC? Não, e o padrão de interface dupla é o enquadramento honesto. Mantenha MVC para o dashboard humano, adicione MVA para a superfície do agente, compartilhe o modelo de domínio entre eles.

O que é um Presenter? Um objeto de nível de domínio que transforma dados crus em percepção do agente: um schema, regras, blocos de UI, uma política de truncamento, ações seguintes, e caminhos de redação. Um por entidade, compartilhado por todas as ferramentas que devolvem essa entidade.

Como MVA reduz alucinação? Removendo as situações em que alucinar é o caminho mais barato. Quando as próximas ações são explícitas, o modelo não inventa nomes de ferramenta. Quando o schema é estrito, campos alucinados são rejeitados com a lista dos válidos. Quando os erros carregam dicas de recuperação, o modelo tenta de novo certo em vez de entrar em loop.

Funciona com o meu modelo? Sim, se o modelo consegue chamar ferramentas. Claude, GPT, Gemini, e qualquer cliente compatível com MCP leem o pacote da mesma forma, porque a camada de percepção mora no servidor.

Por que eu construí

Toda mudança de arquitetura que eu vivi teve o mesmo gatilho: o consumidor mudou, e a arquitetura antiga assumia um consumidor que não existia mais. O navegador fez isso com o desktop. O mobile fez isso com o navegador. O agente está fazendo isso agora, e a suposição que quebrou é uma que nunca escrevemos: que o consumidor preencheria as lacunas.

MVA é o nome para fechar as lacunas na interface em vez de torcer para que o modelo as feche. Escreva o limite, escreva as regras, escreva as ações, e deixe o modelo fazer o que ele é realmente bom.

Tópicosmvaarchitectureagentsmcpmcpfusion