MCP desde cero: construye un servidor listo para producción en TypeScript
Avanzado14 min de lecturaIA para empresas

MCP desde cero: construye un servidor listo para producción en TypeScript

Crear un servidor de Model Context Protocol (MCP) para producción exige mucho más que conectar unas cuantas herramientas. Estos son los patrones de diseño de esquemas, autenticación, gestión de errores, streaming y observabilidad que permiten que un servidor MCP resulte útil a gran escala.

Lo que deberías poder hacer

Un servidor MCP de producción es un servicio pequeño y específico, con herramientas bien diseñadas, esquemas rigurosos, gestión sólida de errores, autenticación adecuada y observabilidad integrada. El protocolo es sencillo; el verdadero trabajo de ingeniería consiste en hacer que el servidor sea útil en flujos de IA reales.

AI Expert TeamPublicado: 15 may 2026
Guardado solo en este navegador.
En este artículo

Para mediados de 2026, MCP (Model Context Protocol) es el estándar de facto para conectar los LLM con herramientas. Anthropic lo introdujo y OpenAI, Google y el resto del ecosistema lo han adoptado. Cursor, Claude Desktop, ChatGPT y los agentes personalizados admiten MCP.

Si quieres que los agentes basados en LLM interactúen con tu servicio, necesitas un servidor MCP. Después de crear uno o dos, comprobarás que el protocolo en sí es reducido. La ingeniería interesante está en todo lo que lo rodea: diseño de esquemas, gestión de errores, autenticación, streaming, rendimiento y observabilidad.

Este artículo profundiza en la creación de servidores MCP de nivel de producción con TypeScript. Trataremos los patrones que resisten el uso real por parte de agentes, no solo la mecánica del protocolo.

Qué es MCP, en pocas palabras

El MCP es un protocolo cliente-servidor donde:

  • Los servidores exponen herramientas, recursos y prompts.
  • Los clientes suelen ser agentes basados en LLM que los consumen.

El protocolo utiliza JSON-RPC 2.0 (la especificación oficial es breve y merece una lectura). Los transportes son stdio para procesos locales y Streamable HTTP para servidores remotos. El antiguo transporte HTTP+SSE quedó obsoleto en la revisión de la especificación 2025-03-26, por lo que cualquier tutorial que lo utilice debe considerarse histórico. La autenticación y la seguridad forman parte del protocolo; las principales implementaciones admiten OAuth, claves de API y mecanismos similares.

La función del servidor es exponer capacidades útiles de forma que los LLM puedan descubrirlas y utilizarlas.

La estructura básica

Con el paquete oficial @modelcontextprotocol/sdk, un servidor mínimo basado en la API de alto nivel McpServer presenta este aspecto:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "my-server",
  version: "1.0.0",
});

server.registerTool(
  "echo",
  {
    description: "Echo back the provided text.",
    inputSchema: { text: z.string() },
  },
  async ({ text }) => ({
    content: [{ type: "text", text }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

(El mismo SDK también expone la clase de bajo nivel Server y setRequestHandler para ListToolsRequestSchema / CallToolRequestSchema si necesitas controlar por completo los manejadores de solicitudes. Sin embargo, en la mayoría de los servidores McpServer.registerTool es más conciso y menos propenso a errores).

Este es el esqueleto. El verdadero trabajo está en qué incluyes en los manejadores de herramientas y cómo lo haces.

Patrón 1: Filosofía del diseño de herramientas

La primera decisión es qué herramientas exponer y con qué granularidad.

Un error habitual consiste en reproducir la API subyacente como un conjunto de herramientas. Si tienes 200 endpoints REST, exponer 200 herramientas es un desastre. Los modelos rinden peor cuando disponen de demasiadas opciones, las descripciones se vuelven inmanejables y el protocolo se convierte en un laberinto.

Es mejor diseñar las herramientas en función de cómo las utilizarán los agentes. Cada una debe realizar una tarea bien definida, aceptar entradas precisas y devolver resultados predecibles.

Algunos principios:

Un concepto por herramienta. No utilices una herramienta manage_customer que realice 12 operaciones distintas. Expón search_customers, get_customer, update_customer_email y archive_customer, cada una con un propósito concreto.

Granularidad adecuada. Si es excesivamente fina, el agente necesitará muchas llamadas; si es demasiado amplia, no podrá ejecutar con precisión la operación necesaria. Piensa en «operaciones a las que una persona pondría nombre».

Verbos de acción. search_documents, no documents. Las herramientas deben nombrarse según lo que hacen.

Distinción entre lectura y escritura. Las herramientas de lectura son más seguras; las de escritura producen efectos secundarios. Distínguelas en el nombre (list_x frente a create_x) y aplica controles distintos, como confirmación explícita o claves de idempotencia.

Agrupa cuando resulte útil. Una herramienta get_customer_profile que devuelve el cliente, sus pedidos recientes y sus tickets de soporte en una sola llamada suele ser preferible a tres llamadas independientes. El agente obtiene el contexto de una vez.

Para un servidor que expone, por ejemplo, un sistema de atención al cliente, un conjunto razonable podría incluir 8-15 herramientas. Más suele ser excesivo.

Patrón 2: Diseño de esquemas

Cada herramienta tiene un esquema de entrada —los parámetros que debe proporcionar el LLM— y un resultado. Los esquemas no solo sirven para validar; también son diseño de prompts.

Usando Zod para esquemas de entrada:

const searchCustomersSchema = z.object({
  query: z.string().describe(
    "Search term: name, email, or company. Be specific to avoid too many matches."
  ),
  limit: z.number().int().min(1).max(50).default(10).describe(
    "Maximum results to return. Default 10, max 50."
  ),
  filters: z.object({
    tier: z.enum(["free", "pro", "enterprise"]).optional().describe(
      "Filter to specific customer tier"
    ),
    status: z.enum(["active", "trial", "churned"]).optional().describe(
      "Filter by customer status"
    ),
  }).optional(),
});

Observa lo siguiente:

  • Cada campo tiene un .describe(). La descripción es lo que el LLM lee.
  • Los valores enumerados son explícitos. Las cadenas de formato libre se restringen siempre que sea posible.
  • Los valores predeterminados son sensatos.
  • Las restricciones (mínimo/máximo, longitud) son explícitas.
  • La distinción entre campos opcionales y obligatorios está clara.

Las descripciones son fundamentales. «search term» aporta poco; «Search term: nombre, correo electrónico o empresa. Sé específico para evitar demasiados resultados» ofrece al LLM una indicación útil.

Patrón 3: Formato de salida

El resultado es lo que el LLM ve y utiliza para decidir su siguiente acción. Diseñarlo bien mejora notablemente su comportamiento.

Salidas estructuradas.

type SearchResult = {
  customers: Customer[];
  total_matches: number;
  truncated: boolean;
  next_page_cursor?: string;
};

Con contexto.

{
  customers: [...],
  total_matches: 47,
  truncated: true,
  next_page_cursor: "abc",
  message: "Found 47 matches; showing first 10. Use next_page_cursor to get more."
}

El campo message es una guía legible por humanos. Los LLM lo usan.

Con una gestión recuperable de errores.

{
  error: "ambiguous_query",
  message: "Search term 'john' matched 247 customers. Please be more specific.",
  suggestion: "Try including a company name or email domain.",
  partial_results: [...]  // top 3 by relevance, optional
}

El error es estructurado —legible por máquina—, pero también incluye un mensaje y una sugerencia que el LLM puede interpretar. Así puede adaptarse, ya sea pidiendo una aclaración al usuario o refinando la consulta.

Con tamaño adecuado.

Una herramienta que devuelve 10,000 registros no es utilizable. No caben en el contexto del LLM y, aunque cupieran, el modelo no los aprovecharía bien. Pagina, trunca o resume siempre. Devuelve lo necesario para tomar una decisión, no todo lo que existe.

Patrón 4: Semántica de errores

Las herramientas fallan. La forma en que comuniquen el error al LLM determina si este puede recuperarse o si agrava el problema.

Categorías de error.

type ToolError =
  | { type: "validation"; message: string; field?: string }
  | { type: "auth"; message: string }
  | { type: "not_found"; message: string; suggestion?: string }
  | { type: "conflict"; message: string; resolution?: string }
  | { type: "rate_limit"; message: string; retry_after_seconds: number }
  | { type: "service_unavailable"; message: string; retryable: boolean }
  | { type: "internal"; message: string; trace_id: string };

Cada categoría tiene diferentes semánticas. El LLM debe responder de forma diferente:

  • validation: corrige la entrada y vuelve a intentarlo.
  • not_found: informa al usuario o intenta una búsqueda diferente.
  • conflict: pide una resolución.
  • rate_limit: espera y vuelve a intentarlo.
  • service_unavailable: intenta una alternativa o notifica al usuario.
  • internal: abandona la operación e informa al usuario.

Documentar estas semánticas permite que el LLM actúe correctamente.

Formato de errores.

Devuelve errores como datos estructurados, con mensajes claros y acciones posibles:

{
  error: {
    type: "validation",
    message: "The email address is not in a valid format.",
    field: "email",
    suggestion: "Provide a valid email address like 'name@example.com'."
  }
}

Evita:

{
  error: "Invalid input"
}

La primera permite que el LLM se recupere. La segunda lo deja adivinando.

Patrón 5: Autenticación y autorización

Los servidores MCP de producción necesitan autenticación. Cualquiera que pueda acceder al servidor podría utilizar sus herramientas, lo que casi siempre supone un problema.

Autenticación: ¿quién está llamando?

Patrones comunes:

  • Clave de API. Sencilla, habitual y adecuada para comunicaciones entre servicios. Emite una por consumidor y rótala periódicamente.
  • OAuth. Adecuado para sistemas multiusuario en los que cada usuario final autoriza al agente. Es más complejo, pero constituye la solución correcta en muchos casos.
  • mTLS. Para entornos de alta seguridad, con certificados TLS mutuos en ambos extremos.

La implementación depende del transporte. Sobre HTTP, autenticas la solicitud antes de que alcance al manejador de MCP (en tu middleware de Express/Hono/Fastify) y guardas al llamante en la solicitud:

// Express-style middleware in front of the MCP HTTP endpoint.
app.use("/mcp", async (req, res, next) => {
  const apiKey = req.header("x-api-key");
  const caller = await authenticate(apiKey);
  if (!caller) return res.status(401).send("Unauthorized");
  (req as any).caller = caller;
  next();
});

Después, cada manejador obtiene la identidad del llamante a partir del contexto de la llamada (extra), no directamente de las cabeceras. En stdio no existen cabeceras HTTP; la autenticación suele proceder del entorno del proceso o de archivos de configuración.

Autorización: ¿qué pueden hacer?

Una vez autenticado, ¿qué herramientas puede usar el llamante y en qué datos?

function authorize(caller: Caller, tool: string, params: any): boolean {
  // Caller-level: can this caller use this tool at all?
  if (!caller.tools.includes(tool)) return false;
  
  // Data-level: is this caller authorized for this specific data?
  if (params.tenant_id && params.tenant_id !== caller.tenant_id) return false;
  
  return true;
}

No permitas que el LLM tome decisiones de autorización: puede ser manipulado. La autorización es responsabilidad del servidor y el LLM solo debe recibir los datos que tiene permiso para ver.

En sistemas multiinquilino, cada llamada queda limitada a un inquilino. Este se determina mediante la autenticación, nunca mediante parámetros proporcionados por el LLM.

Patrón 6: Idempotencia

La idempotencia es esencial en las operaciones de escritura. El LLM puede repetir una llamada o invocar dos veces la misma herramienta en contextos distintos. Sin idempotencia, se crearán duplicados.

Claves de idempotencia.

La herramienta acepta un parámetro idempotency_key. El servidor comprueba si ya ha visto esa clave: si es así, devuelve el resultado almacenado; si no, ejecuta la operación y guarda el resultado.

async function createInvoice(params: {
  amount: number;
  customer_id: string;
  idempotency_key: string;
}) {
  const cached = await idempotencyStore.get(params.idempotency_key);
  if (cached) return cached;
  
  const invoice = await actuallyCreateInvoice(params);
  await idempotencyStore.set(params.idempotency_key, invoice, { ttl: 86400 });
  return invoice;
}

Para el LLM, sugiere esto en la descripción de la herramienta:

"For each unique invoice you create, generate a UUID and pass it as idempotency_key. If you need to retry the operation, use the same UUID to avoid duplicate creation."

Patrón 7: Streaming

En herramientas que generan resultados extensos o tardan en completarse, el streaming mejora la experiencia del usuario. MCP permite enviar notificaciones de progreso desde el manejador mediante el argumento de llamada extra:

server.registerTool(
  "long_running_task",
  { description: "...", inputSchema: { ... } },
  async (input, extra) => {
    await extra.sendNotification({
      method: "notifications/progress",
      params: { progressToken: extra._meta?.progressToken, progress: 0, message: "Starting..." },
    });

    for (const step of steps) {
      await doStep(step);
      await extra.sendNotification({
        method: "notifications/progress",
        params: {
          progressToken: extra._meta?.progressToken,
          progress: step.index / steps.length,
          message: step.name,
        },
      });
    }

    return { content: [{ type: "text", text: JSON.stringify({ result: finalResult }) }] };
  }
);

Usa streaming para:

  • Operaciones de larga duración (>5 segundos).
  • Grandes salidas (para que el LLM comience a procesar mientras aún se está generando).
  • Operaciones con resultados intermedios dignos de mostrar.

No utilices streaming en operaciones sencillas y rápidas: añade complejidad sin aportar valor.

Patrón 8: Caché

Muchas llamadas consultan repetidamente los mismos datos. Una caché puede mejorar de forma notable el rendimiento y reducir la carga del backend.

Caché local. Caché en el propio proceso —por ejemplo, LRU— para datos de uso frecuente.

Caché distribuido. Redis u otro para caché compartido entre instancias del servidor.

Invalidación de la caché. Cuando cambien los datos, elimina las entradas afectadas. Esta es la parte difícil.

TTL. Las entradas caducan después de un periodo definido. Ajústalo según el tipo de datos: los perfiles de clientes pueden permanecer en caché durante horas; los precios, durante minutos.

La caché solo ayuda cuando las llamadas se repiten. Esto es habitual en los servidores MCP, porque los agentes suelen consultar varias veces las mismas entidades durante una sesión.

Un patrón:

async function getCustomerCached(id: string) {
  const cached = await cache.get(`customer:${id}`);
  if (cached) {
    metrics.increment("cache.hit");
    return cached;
  }
  metrics.increment("cache.miss");
  const customer = await db.getCustomer(id);
  await cache.set(`customer:${id}`, customer, { ttl: 300 });
  return customer;
}

Patrón 9: Limitación de tasas

Los agentes basados en LLM pueden generar una carga sorprendente mediante bucles, reintentos y expansión de tareas. Un agente con un comportamiento defectuoso puede llegar a denegar el servicio al backend.

La limitación de tasas por llamante es esencial:

const limiter = new RateLimiter({ 
  windowMs: 60_000, 
  max: 100  // 100 calls/minute per caller
});

server.setRequestHandler(CallToolRequestSchema, async (request, context) => {
  if (await limiter.exceeded(context.caller.id)) {
    return errorResponse("rate_limit", "Too many requests");
  }
  // ...
});

Además de los límites globales, aplica límites por herramienta: algunas operaciones son costosas y deben restringirse con mayor rigor.

Para operaciones con consecuencias (crear registros, enviar mensajes), usa límites más estrictos o requiere flujos de confirmación explícita.

Patrón 10: Recursos

MCP incluye «recursos»: fuentes de datos de solo lectura que el LLM puede explorar y consultar. Son distintos de las herramientas, que se invocan activamente.

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "doc://my-server/handbook",
      name: "Employee Handbook",
      mimeType: "text/markdown",
      description: "Company employee handbook"
    },
    // ...
  ]
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const content = await loadResource(request.params.uri);
  return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: content }] };
});

Los recursos son útiles para:

  • Documentos de referencia que el LLM podría querer navegar.
  • Configuración o datos de contexto.
  • Tablas de búsqueda o esquemas que el LLM podría necesitar.

Los recursos se leen; las herramientas ejecutan acciones. Utiliza el concepto adecuado en cada caso.

Patrón 11: Observabilidad

Se aplican los mismos patrones que en otros sistemas de IA en producción. Instrumenta:

  • Cada llamada a herramienta: marca de tiempo, llamante, herramienta, parámetros, resultado, latencia, estado.
  • Métricas por herramienta: volumen de llamadas, latencia p50/p95, tasa de errores.
  • Métricas por llamante: quién llama, con qué frecuencia.
  • Contexto de trazado: propaga los identificadores de trazado del llamante hasta las llamadas al backend.

Registros estructurados:

logger.info("tool_call", {
  tool: request.params.name,
  caller_id: context.caller.id,
  trace_id: context.trace_id,
  params: redactPII(request.params.arguments),
  duration_ms: duration,
  status: "success"
});

Envía estos registros a la plataforma de observabilidad.

Patrón 12: Versionado

El servidor MCP evolucionará: algunas herramientas cambiarán, se añadirán otras y las antiguas quedarán obsoletas.

Versionado del servidor. El constructor Server recibe una versión. Increméntala cuando haya cambios para que los clientes puedan detectarlos.

Versionado de herramientas. Si la firma cambia de forma incompatible, crea una nueva versión, como search_customers_v2. Mantén la anterior disponible durante un periodo de transición.

Evolución de esquemas. Es seguro añadir campos opcionales. Eliminar campos o cambiar sus tipos constituye un cambio incompatible.

Obsolescencia. Cuando una herramienta quede obsoleta, indícalo en su descripción: «DEPRECATED: usa search_customers_v2 en su lugar».

Para servidores MCP de producción usados por múltiples clientes, el versionado es esencial. Los servidores internos pueden ser más flexibles.

Patrón 13: Pruebas

¿Cómo pruebas un servidor MCP?

Pruebas unitarias. Lógica de cada herramienta, con dependencias simuladas. Pruebas estándar de TypeScript.

Pruebas de esquemas. Comprueban que los esquemas validen correctamente y gestionen los casos extremos, como campos ausentes o tipos incorrectos.

Pruebas de integración. Inicia el servidor, envía solicitudes MCP reales, verifica las respuestas. El @modelcontextprotocol/sdk incluye utilidades de prueba.

Pruebas end-to-end con un LLM real. Son las más difíciles, pero también muy valiosas. Haz que un LLM utilice el servidor para completar tareas realistas y comprueba que elige correctamente las herramientas. Así descubrirás problemas en sus descripciones.

Un entorno de prueba de extremo a extremo (pseudocódigo; el cableado exacto del cliente depende de qué cliente de LLM uses — el SDK de TypeScript de Anthropic, el de OpenAI, o un marco que admita MCP):

// Start your MCP server as a child process or in-memory transport.
const server = await startTestServer();

// Drive an LLM with the MCP tools attached. The exact API depends on the client.
const result = await runAgent({
  mcpServer: server,
  systemPrompt: "You are a customer service agent...",
  userMessage: "Find the customer Alice and check her open tickets",
});

// Inspect the tool calls captured by the server during the run.
expect(server.callLog.map((c) => c.name)).toEqual([
  "search_customers",
  "list_tickets",
]);

Las pruebas end-to-end detectan problemas en las descripciones que las pruebas unitarias no pueden encontrar.

Patrón 14: Despliegue

¿Dónde vive tu servidor MCP?

Stdio (local). El servidor se ejecuta como un proceso; el cliente lo invoca. Ideal para aplicaciones de escritorio (Claude Desktop, Cursor) y herramientas locales.

Streamable HTTP (remoto). El servidor es un servicio de red. Ideal para servicios alojados, infraestructura compartida, acceso multiusuario.

Para servidores de producción:

  • Streamable HTTP suele ser la opción.
  • Despliega como cualquier servicio web: contenedores, balanceo de carga, escalado automático.
  • TLS obligatorio.
  • Comprobaciones de salud para la plataforma de despliegue.
  • Cierre ordenado de las solicitudes en curso.

Patrón 15: Consideraciones de seguridad

Los servidores MCP exponen capacidades a los LLM, y estos pueden ser manipulados. Esto tiene varias implicaciones de seguridad:

Inyección de prompts a través de entradas de herramientas. Una solicitud del usuario podría contener texto que intente engañar al LLM para que llame a herramientas de forma perjudicial. Defensas:

  • Descripciones claras de herramientas sobre su uso esperado.
  • Verificaciones de autorización en el lado del servidor (independientemente de los parámetros decididos por el LLM).
  • Confirmaciones para acciones con consecuencias.

Exfiltración de datos. Se puede abusar de las herramientas que devuelven datos: un atacante podría manipular al LLM para que revele información sensible. Defensas:

  • Verificaciones de autorización.
  • Registro de qué datos accede quién.
  • Detección de patrones de acceso inusuales.

Agotamiento de recursos. Se puede abusar de las herramientas que consumen recursos del backend. Defensas:

  • Limitación de tasas.
  • Límites de recursos por llamada de herramienta.
  • Interruptores de circuito cuando se degrade el backend.

Inyección en los resultados de las herramientas. Un resultado puede contener texto que manipule al LLM cuando este lo lea. Defensas:

  • Sanitizar salidas cuando sea posible.
  • Ten especial cuidado con las herramientas que devuelvan contenido generado por usuarios.

Estas son superficies de ataque reales. Trata un servidor MCP como cualquier API de producción y aplica defensa en profundidad.

Un ejemplo completo: un servidor MCP pequeño pero real

Para juntarlo, un servidor que expone un CRM pequeño:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { db, cache, logger, authenticate } from "./infra.js";

const server = new McpServer({
  name: "crm-server",
  version: "1.0.0",
});

// === Tool: search_customers ===

server.registerTool(
  "search_customers",
  {
    description: "Search customers by name, email, or company.",
    inputSchema: {
      query: z.string().describe("Name, email, or company"),
      limit: z.number().int().min(1).max(50).default(10),
    },
  },
  async ({ query, limit }, extra) => {
    const auth = await authenticate(extra);
    const cacheKey = `search:${auth.tenant_id}:${query}:${limit}`;

    const cached = await cache.get(cacheKey);
    if (cached) return cached;

    const customers = await db.searchCustomers({
      tenant_id: auth.tenant_id,
      query,
      limit,
    });

    const result = {
      content: [{
        type: "text" as const,
        text: JSON.stringify({
          customers,
          total_matches: customers.length,
          truncated: customers.length === limit,
          message:
            customers.length === limit
              ? `Showing first ${limit}; there may be more matches.`
              : `Found ${customers.length} customer(s).`,
        }),
      }],
    };

    await cache.set(cacheKey, result, { ttl: 60 });
    logger.info("search_customers", { tenant: auth.tenant_id, query, results: customers.length });
    return result;
  }
);

// === Tool: get_customer ===

server.registerTool(
  "get_customer",
  {
    description: "Fetch a single customer by id.",
    inputSchema: { customer_id: z.string() },
  },
  async ({ customer_id }, extra) => {
    const auth = await authenticate(extra);
    const customer = await db.getCustomer(auth.tenant_id, customer_id);
    if (!customer) {
      return {
        isError: true,
        content: [{
          type: "text" as const,
          text: `Customer ${customer_id} not found. Use search_customers to find by name or email.`,
        }],
      };
    }
    return { content: [{ type: "text" as const, text: JSON.stringify({ customer }) }] };
  }
);

// === Tool: update_customer_email (with idempotency) ===

server.registerTool(
  "update_customer_email",
  {
    description: "Update a customer's email; pass the same idempotency_key on retry.",
    inputSchema: {
      customer_id: z.string(),
      new_email: z.string().email(),
      idempotency_key: z
        .string()
        .describe("UUID for this update; pass the same value on retry to prevent duplicates"),
    },
  },
  async (params, extra) => {
    const auth = await authenticate(extra);
    // ... idempotency check, validation, update
    return { content: [{ type: "text" as const, text: "ok" }] };
  }
);

// ... more tools ...

// Wire up a remote transport (Streamable HTTP) on a chosen port via your HTTP server of choice.
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
await server.connect(transport);

Esta es una estructura inicial. Aún debes añadir observabilidad, límites de tasa, más herramientas y esquemas más rigurosos, pero contiene los elementos fundamentales.

¿Qué diferencia a los servidores de producción de los demos?

MCP es un protocolo reducido, pero crear un servidor de producción exige trabajo de ingeniería real. La recompensa es un servicio que cualquier agente basado en LLM puede utilizar mediante una integración estandarizada y sin acoplamiento a un modelo concreto.

Los patrones esenciales son herramientas con un propósito claro, esquemas diseñados para los prompts, semántica estructurada de errores, autenticación sólida, idempotencia, observabilidad y seguridad. Omitir cualquiera de ellos conduce a un servidor MCP que fallará en producción.

Constrúyelo, pruébalo con LLM reales e itera sobre las descripciones de las herramientas. El resultado será un servicio que un LLM pueda utilizar con la misma fluidez que una persona y que escale con el rápido crecimiento del ecosistema de agentes de IA.

Leer a continuación

Continúa por el mismo itinerario de aprendizaje con los siguientes artículos prácticos.