Site
Todos los artículos

Publicado 21 sept 202611 min de lectura

Construye tu propio conector MCP: por qué MCP Fusion importa

Lo que hay entre tus datos y la percepción de un agente, y por qué esa brecha es una arquitectura: la división MVA que cierra el egress, el presenter que decide lo que el agente ve, errores que se curan, estado que el agente puede sentir y un deploy que lo entrega todo como un único bundle hasheable.

Renato Marinho

Por Renato Marinho

Founder · Vinkius

The anatomy of an MCP connector: source of truth on the left, the MVA stack in the middle with the model as egress boundary, the presenter as perception and the tools as verbs, and the perception package the agent receives on the right; undeclared fields are stripped at the model layer.

Un conector MCP es una promesa. Le prometes a un sistema que no comparte tu contexto, tu historial ni tu intención que la llamada a billing.void_invoice significa exactamente lo que tú quisiste que significara. El registro de la base de datos no es la respuesta. Es material bruto, y entre él y el agente hay una capa de arquitectura que la mayoría de los servidores hechos a mano se saltan.

Un agente es estocástico por construcción: alucina parámetros, formatea mal las entradas, reintenta sin pensar y pierde contexto entre llamadas. Un servidor crudo trata cada llamada a una tool como independiente, y esa omisión es lo que convierte una demo funcional en un sistema que corrompe datos, quema tokens y falla de formas que nadie puede trazar. Enviar JSON crudo a un agente crea cuatro modos de fallo estructurales: inanición de contexto, ceguera de acción, inconsistencia perceptiva y filtración de seguridad. Son déficits que ninguna cantidad de ingeniería de prompts repara.

Esa es la tesis de este artículo. Si los modos de fallo son estructurales, la solución también tiene que serlo. MCP Fusion la aplica con un patrón que llama MVA, y el patrón es una separación de responsabilidades que el código de aplicación ya conoce, solo apuntado a un consumidor distinto:

  • El Model define qué es el dato y qué puede salir del proceso.
  • El Presenter define lo que el agente percibe de ese dato.
  • Las Tools tienen los verbos: las consultas, las mutaciones y las acciones.

El pipeline MVA: un registro bruto cruza la frontera del model, los campos no declarados se eliminan y el presenter ensambla el paquete de percepción que el agente recibe de verdad

Una regla mantiene la separación honesta, y es una propiedad de seguridad, no una regla de estilo: la dirección. Las Tools importan presenters, los presenters importan models, los models importan el core y nada importa en sentido contrario. La capa que toca tus datos nunca debe ser la capa que un agente puede dirigir.

Model: donde termina la red

En una aplicación normal, el schema valida la entrada. En un conector, el schema también tiene que cerrar la salida, porque el consumidor al otro lado de la red no es un colega leyendo código. Es un modelo de lenguaje que actúa sobre lo que le entregues. defineModel es donde se traza esa frontera, y sus cuatro declaraciones hacen trabajos distintos:

m.casts declara los campos, sus tipos y sus descripciones. Esas descripciones no son documentación para humanos. Se compilan en el momento justo en las reglas de interpretación que el agente recibe con cada respuesta. m.hidden declara los campos que nunca llegan a la red: hashes de contraseñas, flags internos, marcadores de tenant. m.guarded declara los campos que nunca pueden entrar desde un agente. m.fillable declara los perfiles de entrada, create, update y filter, y los parámetros de una tool se derivan de esos perfiles en vez de reescribirse a mano.

La consecuencia es la que importa en producción. Cuando una migración añade una columna a la tabla, esa columna no se fuga. Se queda fuera de la red hasta que alguien la mete en el model y otro, a propósito, la mete en el presenter. Un servidor crudo no tiene esa propiedad. Su salida es el registro serializado, lo que significa que un hash de contraseña, un flag interno y un ID de tenant llegan a la ventana de contexto del agente en cuanto el schema recibe un campo nuevo.

Presenter: la percepción que nunca mostraste

La V de MVA no es para el ojo humano. Es el paquete que el agente percibe, y se ensambla a partir de cuatro capas.

Dato que sobrevivió al firewall. Antes de que nada se serialice, el presenter ejecuta su schema sobre el resultado crudo en modo strip: pase lo que la base de datos devuelva, el agente solo ve la superficie declarada. Es control de egress a nivel de RAM, no una capa de vista, no una plantilla, no una convención. Lo que el schema no conoce no cruza.

Reglas, entregadas en el momento justo. Las descripciones de campos del model se compilan en reglas de sistema adjuntas a esta respuesta, para esta entidad. El agente no arrastra un prompt global de miles de tokens. Recibe las reglas de interpretación de lo que pidió de verdad. El conocimiento de dominio vive en un solo lugar y solo viaja cuando el dominio está en juego.

Un límite operativo. El presenter declara cuántos elementos puede ver este agente en una respuesta y qué se le dice cuando la lista se trunca. Una lista de diez mil filas no es dato para un agente. Es una denegación de servicio vestida de dato. El límite es parte de la percepción, y el aviso de truncamiento es lo que impide que el agente crea haber visto más de lo que vio.

Afordancias. suggestActions le dice al agente qué puede hacer a continuación con lo que acaba de ver. La documentación lo llama HATEOAS para agentes, y aquí es donde muere la ceguera de acción: la respuesta no es un payload, es una posición en un workflow. Los bloques de gráficos y diagramas renderizados por el servidor forman parte del mismo paquete, y son deterministas: el framework los renderiza, el agente los lee y ningún modelo del loop genera los píxeles.

Tools: verbos con intención

Una tool en este framework no es una función con nombre y schema. Es un verbo semántico con una intención por defecto. f.query es de solo lectura. f.mutation es destructiva por defecto. f.action es neutra. No son etiquetas de metadatos. Determinan qué considera la plataforma seguro de reintentar, qué marca el pipeline de observabilidad y qué señala una tool de gobierno cuando una lectura se vuelve escritura.

A partir del verbo, la cadena es deliberadamente pequeña. .fromModel saca la forma de la entrada del perfil fillable del model, de modo que los parámetros de la tool se derivan de la misma declaración que cierra el egress. .returns pega un presenter a la respuesta. .proxy escribe el handler por ti: infiere el método HTTP del verbo, resuelve los parámetros de la ruta desde la entrada y desenvuelve el sobre de la respuesta. Los pasos .with quedan reservados para entradas específicas del dominio que un model no puede expresar.

f.router agrupa verbos bajo un prefijo y hereda middleware y tags para cada uno. f.middleware deriva un contexto tipado aguas abajo, y el identificador de tenant sale de una credencial verificada en ese contexto. Por eso la documentación puede decir, sin matices, que el agente no puede sobreescribirlo. Los límites de concurrencia y los límites de bytes de egress se enganchan a la misma cadena. Y cuando un workflow necesita un prompt en vez de una tool, definePrompt lo construye desde el mismo presenter: las reglas se convierten en el mensaje de sistema, y el dato y la UI en el bloque de usuario. Una sola fuente de verdad, dos superficies.

Errores que guían, estado que el agente puede sentir

Un servidor crudo responde a una llamada mala con un string plano, y la respuesta del agente a un string plano es reintentar, con la misma entrada, y otra vez. Ese loop es donde van a morir los presupuestos de tokens, y donde un reembolso incorrecto se intenta cuatro veces.

La respuesta del framework es un sobre autoreparable. f.error lo construye con un código específico, un mensaje, una sugerencia, una lista de acciones que el agente puede tomar en su lugar, detalles opcionales y una ventana de reintento:

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

Los códigos específicos ganan a los genéricos. AlreadyPaid le dice al agente algo que BAD_REQUEST no puede, y la línea de recuperación elimina la conjetura que hace caros los loops de reintento.

El estado es el otro sentido que un modelo de lenguaje no tiene. Después de una mutación, el agente sigue creyendo que la lista que recuperó antes de la mutación está vigente. La respuesta del framework son señales de sincronización de estado en la respuesta, tomadas del caché de HTTP: una tool marca su resultado como inmutable, volátil o causal. Un resultado inmutable se puede confiar, uno volátil pide que lo vuelvas a consultar y la marca causal dice que después de esta mutación estos otros verbos hay que volver a consultarlos. Es la causalidad, no el tiempo, y es exactamente lo que un agente sin reloj necesita.

Deploy: el conector como bundle

Un conector construido así se entrega como un solo artefacto, y el CLI hace todo el trabajo. mcpfusion deploy empaqueta el servidor en un archivo autocontenido: todas las dependencias inline, los builtins de Node reemplazados por stubs que existen solo para satisfacer al bundler y nunca se llaman, y el transport también reemplazado por stub, porque la plataforma lo provee. El bundle pasa una puerta de tamaño de 1,5 MB en bruto, que es un presupuesto, no una especificación. Luego se comprime, se hashea y va al edge. Si el hash coincide con el desplegado, la plataforma informa de una restauración instantánea: los mismos bytes, sin recarga.

Dos pasos merecen que los entiendas, porque son lo que hace la arquitectura auditable. El CLI ejecuta una segunda compilación introspectiva del bundle: extrae los contratos de las tools, los prompts y el schema de credenciales, y los escribe en un lockfile de capacidades, un snapshot determinista de la superficie de comportamiento del conector. Ese lockfile admite git diff, y un fusion lock --check en CI es la puerta de control: la superficie que tu código expone de verdad se compara con la superficie que comprometiste, y el diff se clasifica como breaking, risky, safe o cosmetic. El protocolo no tiene un mecanismo para detectar drift, y el framework te da uno. Esa es la diferencia entre un despliegue que no puedes auditar y uno que un revisor puede leer.

El mismo bundle habla la era actual del protocolo: stateless, por request, detrás de cualquier balanceador de carga. Y el registry que lo construyó corre sin cambios en stdio y en la era HTTP de 2025. Así es como el desarrollo local y la producción se quedan en el mismo código.

El despliegue en edge tiene tres restricciones, y todas son decisiones de diseño: el set de tools se registra explícitamente, porque el discovery escanea un sistema de archivos que allí no existe. No hay addons nativos. Y nada del bundle toca el proceso. Imports explícitas y registro explícito, y el servidor es válido en edge.

El deploy como artefacto: árbol de código fuente y lockfile a la izquierda, pipeline del CLI en el medio y el mismo registry corriendo en stdio, HTTP y el edge stateless a la derecha

Por qué construir con MCP Fusion

La pregunta detrás de todo lo anterior es la que conviene responder a la cara: por qué no escribir un servidor MCP plano y añadir seguridad donde duela?

Porque los modos de fallo son estructurales, y las soluciones estructurales viven en el framework, no en el handler. Un servidor crudo tiene seis ausencias, y este tiene seis presencias:

  • Fuga lo que devuelve. Este cierra el egress en la capa del model, de modo que una columna nueva no llega a un agente hasta que alguien la declare.
  • No aplica nada. Este congela el registry después del attach y mantiene el orden del pipeline por construcción: la seguridad es forzada, no convencional.
  • No ve su propio drift. Este hashea la superficie de comportamiento en un lockfile y clasifica cada cambio antes del merge.
  • Responde a los errores con strings. Este responde con instrucciones de recuperación y las siguientes acciones disponibles.
  • Es ciego al tiempo. Este lleva señales de invalidación causal en las respuestas.
  • Es una sola superficie, esté donde esté hospedado. Este es un solo registry en stdio, HTTP, el edge de Vinkius y targets serverless, con observabilidad que se mapea a controles SOC 2 y puede reenviar a un SIEM.

Dos multiplicadores cambian la economía del propio trabajo. Puedes generar el conector desde un contrato existente: un spec de OpenAPI o un schema de Prisma se convierte en un servidor completo, con el egress, el aislamiento de tenant y la protección de memoria ya en el código generado, en un solo comando. Y las pruebas son el pipeline de verdad: el paquete de testing corre tu conector en RAM, por el mismo validation, middleware, handler, presenter y egress que corre producción, con cero tokens y determinismo total. Afirmas que el dato no tiene campo secreto, que las reglas llegaron y que el error se clasificó.

El costo honesto es que esto es una arquitectura, no una librería de ayuda. La separación MVA toma unos días en asimilar, y el presupuesto del bundle mantiene las dependencias livianas. Lo que estás comprando es la frontera entre tus datos y la percepción de un agente, aplicada por el framework en vez de por revisión. Para lo que vaya a correr en producción con agentes de otros al otro lado de la red, ese es el punto.

Un conector, de la especificación al edge

Todo el workflow, de punta a punta:

mcpfusion create invoices --vector openapi
mcpfusion remote --server-id <uuid from the dashboard>
mcpfusion deploy

Para un arranque desnudo en vez de uno generado, los mismos tres comandos con --vector vanilla. Luego el mínimo de tres declaraciones:

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

La prueba corre el mismo pipeline que corre producción y demuestra, sin un solo token, que el egress aguantó. Despliegas, verificas el lockfile en CI, y el conector pasa a ser un hash en un repositorio, un diff en un pull request y un programa aislado en el edge. El mismo objeto, en las tres formas.

El runtime que al final ejecuta ese programa, sellado y restaurado por snapshot, es el tema del artículo sobre los V8 isolates.

Temasmcpfusionconnectorsmcpagentsedge