Cuando el diseño técnico de Entiscore empezó a tomar forma, el documento original proponía resolver el acceso a datos externos mediante un servidor MCP corriendo con stdio transport dentro de una función serverless de Vercel. Sobre el papel era una decisión limpia, una forma directa de decir "usamos MCP" y dejarlo asentado en la arquitectura desde el principio.

El problema apareció en cuanto empezamos a pensar cómo se ejecutaría eso en la práctica.

Por qué el transporte stdio no funciona en una función serverless

El transporte stdio de Model Context Protocol está pensado para comunicación entre dos procesos separados, un cliente que habla con un servidor a través de entrada y salida estándar, cada uno corriendo de forma independiente. Una función serverless de Vercel no tiene esa opción. Cada invocación arranca, ejecuta y termina dentro de un ciclo de vida acotado, sin margen para levantar un segundo proceso que se comunique por stdio con el primero y se mantenga vivo entre requests.

Forzar ese patrón en ese entorno hubiera significado pelear contra la infraestructura en vez de aprovecharla, y con dos días de desarrollo por delante, ese no era un intercambio que valiera la pena hacer, no en ese momento.

La decisión

La solución fue separar el problema en dos capas con propósitos distintos.

Las herramientas de acceso a datos externos que Entiscore necesita, obtener el HTML de un sitio, leer su robots.txt, verificar si un enlace externo responde, se implementaron como funciones TypeScript comunes. Cada una respeta exactamente el contrato de input y output que MCP define para sus tools. El orquestador del agente las invoca directamente, en el mismo proceso, sin ningún protocolo de transporte entre medio. Desde afuera parece una llamada a función normal, pero el contrato de datos que entra y sale de cada una es idéntico al que tendría si viviera detrás de un servidor MCP.

// Contrato de tool que respeta el shape esperado por MCP
export async function fetchSiteHtml(input: { url: string }): Promise<{
  content: string;
  statusCode: number;
  error?: string;
}> {
  try {
    const response = await fetch(input.url, {
      headers: { "User-Agent": "Entiscore/1.0" },
      signal: AbortSignal.timeout(10000),
    });
    const content = await response.text();
    return { content, statusCode: response.status };
  } catch (err) {
    return { content: "", statusCode: 0, error: String(err) };
  }
}

Esa misma lógica se expuso una segunda vez como un servidor MCP, usando el SDK oficial del protocolo. Ese servidor no corre en producción sino durante el desarrollo, para que Kiro pueda conectarse a él como cliente MCP mientras se escribe código, viendo las mismas herramientas, con el mismo contrato, que después el orquestador usará en producción de forma directa.

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 
const server = new Server(
  { name: "entiscore-tools", version: "1.0.0" },
  { capabilities: { tools: {} } }
);
 
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "fetch_site_html",
      description: "Obtiene el contenido HTML de una URL dada",
      inputSchema: {
        type: "object",
        properties: {
          url: { type: "string", description: "La URL a consultar" },
        },
        required: ["url"],
      },
    },
  ],
}));
 
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "fetch_site_html") {
    const result = await fetchSiteHtml(request.params.arguments as { url: string });
    return {
      content: [{ type: "text", text: JSON.stringify(result) }],
    };
  }
  throw new Error(`Tool desconocida: ${request.params.name}`);
});
 
const transport = new StdioServerTransport();
await server.connect(transport);

El resultado es que el proyecto demuestra el uso de MCP en dos frentes distintos y complementarios: el patrón simplificado que resuelve el problema de infraestructura en producción, y el servidor que cumple el propósito original del protocolo durante el desarrollo con Kiro.

Qué produce este enfoque en la práctica

Las tools se definen una sola vez en TypeScript, con un contrato que respeta las expectativas de MCP. El orquestador las usa directamente en el mismo proceso durante producción. Kiro las usa a través del servidor MCP durante el desarrollo. Cuando una tool necesita cambiar, el cambio ocurre en un solo lugar y ambos consumidores lo ven.

El servidor de desarrollo también le dio a Kiro información precisa y en tiempo real sobre lo que las tools devolvían durante la generación de código, no tipos inferidos ni documentación, sino comportamiento en runtime. Eso redujo el número de iteraciones necesarias para que el orquestador llamara a las tools correctamente.

Qué se transfiere más allá de este proyecto

Adoptar un protocolo no significa replicar su implementación de referencia sin cuestionar si el entorno donde va a correr la soporta tal cual. El contrato de datos de MCP, la forma en que se estructura el input y el output de cada herramienta, es independiente del transporte que se elija para moverlo de un lado a otro.

Se puede respetar ese contrato, preservar la interoperabilidad conceptual que ofrece el protocolo, y al mismo tiempo elegir un mecanismo de invocación que encaje con las restricciones del entorno donde el sistema va a vivir. En el caso de Entiscore, eso significó llamadas directas en el mismo proceso en producción y transporte stdio solo en desarrollo, donde las restricciones que hacían impracticable stdio en producción simplemente no aplican.


Entiscore está disponible en entiscore.vercel.app. Construido con Next.js, TypeScript, Supabase y Claude API para el hackathon Kiro powered by AWS de Código Facilito.