Publicado 21 de set. de 202611 min de leitura
Construindo o seu conector MCP: por que o MCP Fusion importa
O que existe entre os seus dados e a percepção de um agente, e por que esse intervalo é uma arquitetura: a divisão MVA que fecha o egresso, o presenter que decide o que o agente vê, erros que se curam, estado que o agente sente e um deploy que entrega tudo como um único bundle hashável.

Por Renato Marinho
Founder · Vinkius
Um conector MCP é uma promessa. Você está prometendo a um sistema que não compartilha o seu contexto, a sua história nem a sua intenção que uma chamada a billing.void_invoice signifique exatamente o que você quis dizer. A linha do banco de dados não é a resposta. É material bruto, e entre ela e o agente existe uma camada de arquitetura que a maioria dos servidores feitos à mão pula.
Um agente é estocástico por construção: ele alucina parâmetros, formata mal as entradas, tenta de novo sem pensar e perde contexto entre as chamadas. Um servidor cru trata cada chamada de ferramenta como um evento independente, e é esse único ponto cego que transforma um demo funcionando num sistema que corrompe dados, queima tokens e falha de formas que ninguém consegue rastrear. Enviar JSON cru a um agente cria quatro modos de falha estruturais, fome de contexto, cegueira de ação, inconsistência de percepção e vazamento de segurança, e são déficits que nenhuma quantidade de engenharia de prompt corrige.
Esse é o tema deste post. Se os modos de falha são estruturais, o remédio também tem de ser estrutural. O MCP Fusion faz esse remédio com um padrão que ele chama de MVA, e o padrão é uma separação de responsabilidades que o código de aplicação já conhece, só apontada para um consumidor diferente:
- O Model define o que os dados são e o que pode sair do processo.
- O Presenter define o que o agente percebe sobre eles.
- As Ferramentas definem os verbos: as consultas, as mutações e as ações.
Uma regra mantém a separação honesta, e ela é uma propriedade de segurança, não uma questão de estilo: a direção. Ferramentas importam presenters, presenters importam models, models importam o core, e nada importa para trás. A camada que toca os seus dados nunca pode ser a camada que um agente dirige.
Model: onde o fio termina
Numa aplicação comum, o schema valida a entrada. Num conector, o schema tem de fechar a saída também, porque o consumidor do outro lado do fio não é um colega lendo código. É um modelo de linguagem que vai agir com o que lhe entregarem. É no defineModel que essa fronteira é desenhada, e as suas quatro declarações fazem trabalhos diferentes:
m.casts declara os campos, os tipos e as descrições. As descrições não são documentação para humanos. Elas compilam, na hora exata, nas regras de interpretação que o agente recebe em cada resposta. m.hidden declara os campos que nunca chegam ao fio: hashes de senha, flags internas, marcadores de tenant. m.guarded declara os campos que nunca podem entrar vindos de um agente. m.fillable declara os perfis de entrada, create, update e filter, e os parâmetros de uma ferramenta são derivados desses perfis, e não digitados de novo à mão.
A consequência é a que importa em produção. Quando uma migração adiciona uma coluna à tabela, a coluna não vaza. Ela fica fora do fio até alguém colocá-la no model e alguém, de propósito, colocá-la no presenter. Um servidor cru não tem essa propriedade. A saída dele é a linha, serializada, o que quer dizer que um hash de senha, uma flag interna e um tenant ID chegam todos à janela de contexto do agente no instante em que um novo campo aterrissa no schema.
Presenter: a percepção que você nunca mostrou
O V do MVA não é para o olho humano. É o pacote que o agente percebe, e ele é montado em quatro camadas.
Dados que sobreviveram ao firewall. Antes de qualquer coisa ser serializada, o presenter roda o schema sobre o resultado bruto em modo strip: o que o banco tiver devolvido, o agente só vê a superfície declarada. É controle de egresso em nível de RAM, não uma camada de view, não um template, não uma convenção. Um campo que o schema não conhece não cruza.
Regras entregues na hora certa. As descrições dos campos do model compilam em regras de sistema anexadas a esta resposta, desta entidade. O agente não carrega um prompt global de milhares de tokens; ele recebe as regras de interpretação do que ele realmente pediu. O conhecimento de domínio vive num lugar só e embarca só quando o domínio está em jogo.
Um limite de trabalho. O presenter declara quantos itens este agente pode ver numa resposta e o que ele é informado quando a lista é truncada. Uma lista de dez mil linhas não é dado para um agente. É uma negação de serviço vestindo fantasia de dado. O limite faz parte da percepção, e o aviso de truncamento é o que impede o agente de fingir que viu mais do que viu.
Afinidades. O suggestActions diz ao agente o que ele pode fazer a seguir com o que acabou de ver. Os docs chamam isso de HATEOAS para agentes, e é aqui que a cegueira de ação morre: a resposta não é um payload, é uma posição num fluxo. Blocos de gráfico e diagrama renderizados no servidor fazem parte do mesmo pacote, e são determinísticos: o framework os renderiza, o agente os lê, e nenhum modelo no loop gera os pixels.
Ferramentas: verbos com intenção
Ferramenta neste framework não é uma função com nome e schema. É um verbo semântico com intenção padrão. f.query é somente leitura. f.mutation é destrutivo por padrão. f.action é neutro. Isso não são rótulos de metadados. Isso define o que a plataforma trata como seguro para repetir, o que o pipeline de observabilidade marca e o que uma ferramenta de governança acusa quando uma leitura vira escrita.
A partir do verbo, a cadeia é deliberadamente curta. .fromModel puxa a forma da entrada do perfil fillable do model, então os parâmetros da ferramenta são derivados da mesma declaração que fecha o egresso. .returns anexa o presenter à resposta. .proxy escreve o handler por você: infere o método HTTP a partir do verbo, resolve os parâmetros de caminho a partir da entrada e desembrulha o envelope da resposta. Os passos .with ficam reservados para entradas específicas de domínio que o model não sabe expressar.
f.router agrupa verbos sob um prefixo e herda middlewares e tags para cada um. f.middleware deriva um contexto tipado a jusante, e o identificador de tenant vem de uma credencial verificada naquele contexto, o que explica por que os docs podem dizer, sem rodeios, que o agente não consegue sobrescrevê-lo. Limites de concorrência e teto de bytes de egresso se ligam na mesma cadeia. E quando o fluxo precisa de um prompt em vez de uma ferramenta, o definePrompt o constrói a partir do mesmo presenter: as regras viram a mensagem de sistema, os dados e a UI viram o bloco do usuário. Uma fonte da verdade, duas superfícies.
Erros que guiam, estado que o agente sente
Um servidor cru responde a uma chamada ruim com uma string plana, e a resposta do agente a uma string plana é tentar de novo, com a mesma entrada, e de novo. É nesse loop que orçamento de tokens morre, e é nele que um estorno errado é tentado quatro vezes.
A resposta do framework é um envelope que se cura sozinho. O f.error o constrói a partir de um código específico, uma mensagem, uma sugestão, uma lista de ações que o agente pode tomar em vez, detalhes opcionais e uma janela de retry:
<tool_error code="InvoiceNotFound">
<message>Invoice "INV-999" does not exist.</message>
<recovery>Call billing.list_invoices first to find valid IDs.</recovery>
<available_actions>billing.list_invoices</available_actions>
</tool_error>
Códigos específicos vencem os genéricos. AlreadyPaid diz ao agente algo que BAD_REQUEST não diz, e a linha de recovery remove a adivinhação que torna os loops de retry caros.
Estado é o outro sentido que um modelo de linguagem não tem. Depois de uma mutação, o agente continua acreditando que a lista que ele buscou antes da mutação está atualizada. A resposta do framework são sinais de state sync na resposta, emprestados do cache do HTTP: uma ferramenta marca o resultado como imutável, volátil ou causal. Um resultado imutável pode ser confiado, um volátil diz requise de novo, e uma marca causal diz que, depois desta mutação, estes outros verbos precisam ser requeridos. É causalidade, não tempo, que é exatamente o que um agente sem relógio precisa.
Deploy: o conector como bundle
Um conector feito assim viaja como um único artefato, e o CLI faz o trabalho inteiro. O mcpfusion deploy embala o servidor num arquivo autossuficiente: todas as dependências embutidas, os builtins do Node substituídos por stubs que existem apenas para satisfazer o bundler e nunca são chamados, e o próprio transporte também stubado, porque a plataforma é quem o fornece. O bundle passa por um controle de tamanho de 1,5 MB bruto, que é um orçamento, não uma especificação, depois é comprimido e hashado, e vai para a edge. Se o hash for o mesmo do já publicado, a plataforma reporta um restore instantâneo: os mesmos bytes, sem recarga.
Dois passos merecem ser entendidos, porque são eles que tornam a arquitetura auditável. O CLI roda uma segunda compilação, introspectiva, do bundle: extrai os contratos das ferramentas, os prompts e o schema de credenciais, e escreve tudo num capability lockfile, um snapshot determinístico da superfície comportamental do conector. Esse lockfile é diffável no git, e um fusion lock --check no CI é o gate: a superfície que o seu código realmente expõe é comparada à superfície que você versionou, e o delta é classificado como breaking, risky, safe ou cosmetic. O protocolo não tem nenhum mecanismo para detectar drift, e o framework fornece um. Essa é a diferença entre um deploy que ninguém audita e um que um revisor consegue ler.
O mesmo bundle fala a era atual do protocolo: stateless, por requisição, atrás de qualquer balanceador. E o registry que o construiu roda inalterado em stdio e na era 2025 do HTTP, que é como o desenvolvimento local e a produção continuam no mesmo código.
O deploy de edge tem três restrições, e todas são decisões de projeto: o conjunto de ferramentas é registrado explicitamente, porque a descoberta varre um filesystem que não existe ali; não há addons nativos; e nada no bundle toca o processo. Importações explícitas e registro explícito, e o servidor vira legal para edge.
Por que construir com MCP Fusion
A pergunta por trás de tudo acima é a que vale responder de frente: por que não escrever um servidor MCP comum e adicionar segurança onde dor começa?
Porque os modos de falha são estruturais, e remédio estrutural mora no framework, não no handler. Um servidor cru tem seis ausências, e este tem seis presenças:
- Ele vaza o que retorna. Este fecha o egresso na camada do model, então uma coluna nova não alcança um agente até alguém declará-la.
- Ele não aplica nada. Este congela o registry após o attach e mantém a ordem do pipeline por construção: a segurança é aplicada, não convencional.
- Ele não vê o próprio drift. Este faz o hash da superfície comportamental num lockfile e classifica cada mudança antes do merge.
- Ele responde a erro com string. Este responde com instruções de recovery e as próximas ações disponíveis.
- Ele é cego para o tempo. Este carrega sinais de invalidação causal nas respostas.
- Ele é uma superfície só, onde quer que fique hospedado. Este é um registry em stdio, HTTP, a edge do Vinkius e alvos serverless, com observabilidade que mapeia para controles SOC 2 e consegue forward para um SIEM.
Dois multiplicadores mudam a economia do próprio trabalho. Dá para gerar o conector a partir de um contrato existente: uma spec OpenAPI ou um schema Prisma viram um servidor completo, com o egresso, o isolamento de tenant e a proteção de memória já costurados no código gerado, num comando só. E os testes são o pipeline de verdade: o pacote de testing roda o seu conector em RAM, pelos mesmos passos de validação, middlewares, handler, presenter e egresso que a produção roda, com zero tokens e determinismo total. Você afirma que os dados não têm campo secreto, que as regras chegaram e que o erro se classificou.
O custo honesto é que isso é uma arquitetura, não uma biblioteca de utilidades. A divisão MVA leva alguns dias para virar reflexo, e o orçamento de bundle mantém as dependências enxutas. O que você está comprando é a fronteira entre os seus dados e a percepção de um agente, aplicada pelo framework em vez de por revisão. Para qualquer coisa que vai rodar em produção com agentes de outras pessoas do outro lado do fio, isso é o ponto.
Um conector, da spec à edge
O fluxo inteiro, de ponta a ponta:
mcpfusion create invoices --vector openapi
mcpfusion remote --server-id <uuid from the dashboard>
mcpfusion deploy
Para começar do zero em vez de gerar, os mesmos três comandos com --vector vanilla. Depois, o mínimo de três declarações:
defineModel('Invoice', m => {
m.casts({
id: m.uuid().label('Invoice ID, a UUID'),
total: m.number().label('Total, integer cents'),
status: m.string().label('open, paid, or voided'),
});
m.hidden(['webhookSecret', 'internalFlags']);
m.fillable({ create: ['id', 'total'], filter: ['status'] });
});
const router = f.router('billing');
router.mutation('void_invoice')
.withString('id')
.returns(invoiceUI)
.invalidates('billing.*')
.proxy('invoices/:id/void');
const tester = createMCPFusionTester(registry, {
contextFactory: () => ({ tenantId: 't_777' }),
});
const result = await tester.callAction('billing', 'void_invoice', { id: 'INV-7' });
expect(result.data).not.toHaveProperty('webhookSecret');
expect(result.uiBlocks.length).toBeGreaterThan(0);
O teste roda o mesmo pipeline que a produção roda e prova, sem um único token, que o egresso segurou. Deploy, confira o lockfile no CI, e o conector é um hash num repositório, um delta num pull request e um programa isolado na edge. O mesmo objeto, nas três formas.
O runtime que acaba por executar esse programa, selado e restaurado por snapshot, é o assunto do post sobre isolates V8.
