Inicio / Artículos / MCP para agentes de IA: Estandarizando la integración de herramientas en LangGraph

MCP para agentes de IA: Estandarizando la integración de herramientas en LangGraph

Este artículo explica qué es lo que MCP realmente estandariza en los sistemas de IA agentes, comparando las integraciones de herramientas ad hoc con las basadas en MCP dentro de un orquestador LangGraph.

4323 palabras

Introducción y resumen

Al final de la Parte 2, el sistema había alcanzado un estado verdaderamente coordinado: un conjunto de agentes especializados, cada uno responsable de una tarea específica, que operaban sobre un estado compartido bajo la dirección de un orquestador que decidía qué debía ejecutarse a continuación. Sin embargo, en cada ejemplo se partió de la suposición de que cada agente ya tenía acceso a todo lo que necesitaba dentro de ese estado compartido, pudiéndolo leer cuando fuera necesario.

Esa suposición rara vez se cumple en entornos de producción. Un agente suele necesitar algo que esté fuera del grafo: una fila de una base de datos, una respuesta de una API externa, un fragmento extraído de una base de conocimientos o algún otro recurso que se encuentre más allá de los límites del propio sistema. Cada vez que un agente necesita acceder a elementos externos de esta manera, necesita su propio camino para hacerlo, y si cada uno de esos caminos se construye a mano, terminas repitiendo el mismo tipo de trabajo de integración, ligeramente diferente, para cada agente y cada recurso externo.

Esa repetición es exactamente el problema que aborda esta sección, y explica por qué MCP se ha convertido en un tema tan recurrente durante el último año. Antes de decidir si vale la pena adoptarlo, es útil determinar con precisión qué problema resuelve y analizar qué implicaba conectar una herramienta a un agente antes de que existiera MCP.

El problema que MCP afirma resolver

Antes de MCP, para darle a un agente acceso a algo externo era necesario crear manualmente una integración personalizada adaptada a ese recurso específico, diseñada de la forma que pareciera razonable en ese momento. Un recurso podía ser accesible a través de una API REST, otro mediante un cliente de base de datos, y otro aún a través de un SDK que incluía sus propias reglas para la autenticación y el manejo de errores. Cada una de estas diferencias tenía que ser incorporada directamente en el código del propio agente.

Eso es un buen equilibrio cuando hay un único agente que interactúa con una sola herramienta. Deja de serlo en cuanto el sistema crece en cualquiera de sus dimensiones. Si se agrega un segundo agente que necesita el mismo recurso, o bien se copia la integración o alguien acaba por extraerla en un módulo compartido, generalmente solo después de que la duplicación ya se haya producido. En cambio, si se agrega una segunda herramienta, ahora el código del agente tiene que contener al mismo tiempo dos patrones de integración completamente diferentes.

Estas integraciones rara vez se parecen entre sí, porque nada lo exige. Un wrapper podría volver a intentar las llamadas fallidas automáticamente; otro podría no hacerlo en absoluto. Uno podría mostrar los fallos como excepciones lanzadas; otro podría ocultarlos en un campo de estado que quien llama debe recordar verificar. No existe un lenguaje común para definir qué significa realmente “conectar a un agente con una herramienta”; cada integración acaba respondiendo a esa pregunta según sus propios términos.

Este es precisamente el vacío que MCP se propone cerrar. No introduce nuevas capacidades que los agentes carecían antes; estandariza el mecanismo para acceder a capacidades que ya existían, de modo que conectar un nuevo agente a una herramienta existente, o una nueva herramienta a un agente existente, ya no signifique tener que escribir otra integración personalizada desde cero. Resulta mucho más fácil juzgar si cumple esa promesa en la práctica una vez que se ha visto cómo es en realidad el enfoque ad hoc en código, y ahí es donde continúa la discusión.

Antes de MCP: Conectar una herramienta de forma improvisada

Considere una integración bastante común: un agente que necesita consultar un sistema externo, mantenido unido por cualquier código de enlace que haga funcionar la conexión.

import requests
class LookupToolClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key
    def lookup(self, query: str) -> dict:
        response = requests.get(
            f"{self.base_url}/search",
            params={"q": query},
            headers={"Authorization": f"Bearer {self.api_key}"},
        )
        if response.status_code != 200:
            return {"error": f"lookup failed: {response.status_code}"}
        return response.json()
def agent_node(state: GraphState) -> dict:
    client = LookupToolClient(base_url="https://internal-tool.example.com", api_key="...")
    result = client.lookup(state["extracted_fields"]["query"])
    return {"tool_result": result}

Por sí mismo, no hay nada malo en esto. Se trata de un cliente HTTP compacto, algo de manejo de errores y una función que lo invoca desde dentro de un nodo. Los problemas comienzan cuando entra en escena una segunda herramienta: esta vez no se trata de otra API REST, sino de un cliente de base de datos con una estructura completamente diferente:

import psycopg2
class RecordsClient:
    def __init__(self, connection_string: str):
        self.conn = psycopg2.connect(connection_string)
    def fetch_record(self, record_id: str) -> dict:
        with self.conn.cursor() as cur:
            cur.execute("SELECT * FROM records WHERE id = %s", (record_id,))
            row = cur.fetchone()
            if row is None:
                raise ValueError(f"no record found for {record_id}")
            return dict(zip([desc[0] for desc in cur.description], row))

Estos dos clientes no comparten ninguna interfaz común, ni convención de nomenclatura, ni siquiera un método consistente para indicar fallos: uno devuelve un diccionario de errores, mientras que el otro lanza una excepción directamente. Cualquier agente que necesite utilizar ambos debe aprender estas particularidades por separado y gestionar cada una según sus propias reglas. Ahora multiplique eso por todas las herramientas adicionales que el sistema llegue a necesitar: su propio cliente, su propio esquema de autenticación, su propio modo de fallo… Y lo que comenzó como un par de integraciones simples se convierte en una verdadera carga de mantenimiento, sin ninguna estructura compartida que las unifique.

Esa es la base a tener en cuenta aquí: no se trata de una integración mal escrita, sino de una típica, construida de la forma en que suelen quedar las integraciones entre herramientas cuando no existe nada que imponga una estructura común.

Qué estandariza realmente MCP

Teniendo aún en mente ese ejemplo ad hoc, resulta mucho más fácil describir con precisión qué hace MCP, sin recurrir a las afirmaciones más amplias y vagas que a menudo se hacen al respecto.

En esencia, MCP define un protocolo compartido para exponer herramientas a un agente, independientemente de lo que haga internamente dicha herramienta o del lenguaje o framework con el que esté desarrollada. En lugar de que cada herramienta incluya su propio cliente personalizado con sus propias convenciones, se expone a través de un servidor MCP que anuncia sus capacidades en un formato estándar y predecible: un nombre, una descripción, un esquema de entrada y un esquema de salida. Cualquier agente que entienda el protocolo puede descubrir esa herramienta y llamarla de la misma manera que llamaría a cualquier otra herramienta, ya sea que lo que hay detrás sea un endpoint REST, una base de datos o algo completamente distinto.

Esa estandarización abarca exactamente tres áreas, y vale la pena ser específico sobre cuáles son, ya que es tentador asumir que el alcance de MCP es más amplio de lo que realmente es.

  • Detección: Un agente puede consultar a un servidor MCP para obtener la lista de herramientas que pone a disposición y recibir una respuesta estructurada, en lugar de depender de que esa información esté codificada de forma fija en algún lugar o documentada por separado de la implementación real.
  • Invocación: Cada llamada a una herramienta sigue el mismo patrón, independientemente de cuál sea la herramienta: una solicitud con una estructura definida y una respuesta con una estructura definida, en lugar de que cada cliente defina su propia firma de método y su propio tipo de retorno.
  • Gestión de errores: Los fallos se informan en un formato uniforme, por lo que un agente no necesita seguir si una herramienta determinada lanza una excepción, devuelve un campo de error o falla de alguna otra manera. La estructura es siempre la misma.
  • Lo que MCP no hace es eliminar el trabajo de escribir la lógica subyacente de una herramienta, ni garantiza que una herramienta se comporte correctamente solo porque está encapsulada en el protocolo. Estándariza el contrato entre un agente y una herramienta, pero no la calidad o fiabilidad de lo que hay detrás de ese contrato; este es un punto al que el artículo vuelve directamente más adelante, una vez que las comparaciones anteriores han dejado clara la distinción.

    Anatomía de un servidor MCP

    Dada la estandarización que acabamos de describir, vale la pena analizar qué es lo que realmente la implementa. Estructuralmente, un servidor MCP no es más que un conjunto declarado de herramientas, cada una con su propio esquema, empaquetadas dentro de una capa de protocolo que permite a un agente descubrirlas e invocarlas de manera consistente.

    Como mínimo, para definir una herramienta en un servidor MCP se requiere declarar tres elementos: un nombre que el agente utiliza para referirse a ella, un esquema que describe la entrada esperada y la función que se ejecuta cuando se invoca realmente la herramienta.

    from mcp.server import Server
    from mcp.types import Tool
    server = Server("lookup-tools")
    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"],
                },
            )
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        raise ValueError(f"unknown tool: {name}")
    

    Al comparar esto con los clientes ad hoc mencionados anteriormente, destacan dos aspectos importantes. Primero, el esquema de entrada se declara explícitamente desde el principio, en lugar de inferirse a partir de los parámetros que reciba una función; esto permite que tanto un agente como un revisor humano vean con exactitud qué requiere una herramienta sin necesidad de analizar su implementación. Segundo, el servidor solo necesita exponer dos puntos de entrada: list_tools y call_tool, independientemente de cuántas herramientas contenga o de cómo difieran en su funcionamiento interno. Si la herramienta lookup se comunica con una API REST, consulta una base de datos o realiza alguna otra acción, todo permanece completamente oculto tras esa misma interfaz formada por dos funciones.

    Por parte del agente, conectarse a este servidor se ve idéntico sin importar qué herramientas exponga:

    from mcp.client import ClientSession
    async def call_lookup_tool(query: str) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool("lookup", {"query": query})
            return result
    

    Compare esto con los dos clientes ad hoc anteriores, uno basado en requests y el otro en psycopg2, cada uno con su propia estructura y convenciones distintas. Aquí, el código del agente permanece igual independientemente de lo que haga internamente una herramienta: llama a session.call_tool con un nombre y un conjunto de argumentos, y recibe un resultado en la misma estructura cada vez. Esa uniformidad es el verdadero beneficio de la arquitectura del servidor, no la lógica de la herramienta en sí, que de todas formas debe ser escrita por alguien.

    Volver a diseñar la misma integración mediante MCP

    La mejor manera de observar la diferencia práctica es tomar exactamente la misma integración desarrollada anteriormente, la herramienta de búsqueda y el cliente de registros, y reconstruirla utilizando MCP en su lugar. La funcionalidad permanece idéntica, los sistemas subyacentes también siguen siendo los mismos; solo cambia la interfaz.

    La herramienta de búsqueda, que inicialmente era un cliente independiente basado en requests, ahora se convierte en una declaración de herramienta registrada en un servidor MCP:

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"],
                },
            ),
            Tool(
                name="fetch_record",
                description="Fetch a record by ID",
                inputSchema={
                    "type": "object",
                    "properties": {"record_id": {"type": "string"}},
                    "required": ["record_id"],
                },
            ),
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        elif name == "fetch_record":
            return fetch_record_from_db(arguments["record_id"])
        raise ValueError(f"unknown tool: {name}")
    

    Ninguna parte de la lógica interna fue modificada: perform_lookup sigue accediendo a la misma API externa, y fetch_record_from_db sigue ejecutando la misma consulta a la base de datos. Lo que cambia es que estas dos herramientas, aunque operan sobre sistemas completamente diferentes, ahora se declaran una junto a la otra, comparten la misma estructura de esquema e interfazán a través de los mismos dos puntos de entrada.

    El cambio más notable ocurre en el nodo encargado de llamarlos, ya que ya no necesita tener conocimiento alguno sobre requests ni psycopg2:

    async def agent_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["query"]}
            )
        return {"tool_result": result}
    

    Esto contrasta con la versión anterior, que importaba una biblioteca HTTP específica,解析aba un formato de error concreto y requería un camino de código completamente separado solo para llamar a fetch_record en lugar de lookup. En esta versión, invocar una segunda herramienta consiste simplemente en intercambiar una cadena y un diccionario de argumentos, en lugar de crear un segundo cliente con sus propias particularidades.

    Nada de lo que realmente logran estas herramientas ha cambiado. Lo que sí ha cambiado es que el agente que las utiliza ya no necesita comprender en absoluto su implementación, solo su nombre y el esquema declarado. Esa es la estandarización de la que se habló anteriormente en esta serie; ahora es algo a lo que se puede hacer referencia directamente en el código, en lugar de confiar ciegamente.

    Comparación directa: Ad Hoc vs. MCP

    Una vez que ambas aproximaciones estén completamente desarrolladas, podrás dejar de analizar beneficios teóricos y centrarte directamente en las diferencias reales entre la versión ad hoc y la versión MCP.

    • Esfuerzo requerido por cada herramienta nueva: En el enfoque ad hoc, cada herramienta adicional traía consigo su propio cliente, su propia lógica de autenticación, su propio formato de errores y su propio conjunto de convenciones que había que aprender antes de poder utilizarla con seguridad. Bajo MCP, agregar una herramienta significa incluir una entrada más en list_tools y un ramal adicional dentro de call_tool, cada uno siguiendo el mismo patrón que la herramienta anterior.
    • Qué debe entender el código que realiza la llamada: El nodo de agente ad hoc tenía que importar una biblioteca de cliente específica y manejar el formato de errores particular de dicha biblioteca. El nodo de agente MCP no importa nada específico de la herramienta; simplemente llama a session.call_tool con un nombre y un diccionario de argumentos, y esa llamada se comporta de la misma manera independientemente de si la herramienta subyacente es un endpoint REST, una base de datos o cualquier otra cosa.
  • Nivel de reutilización de la configuración entre agentes: En el diseño ad hoc, si un segundo agente necesita la misma capacidad de búsqueda, debe importar directamente el mismo cliente y quedar vinculado a esa implementación específica, o bien alguien tendrá que crear un contenedor compartido para evitar la duplicación. Con un servidor MCP, un segundo agente simplemente se conecta a dicho servidor y hereda el mismo conjunto de herramientas, que pueden ser descubiertas y llamadas de la misma manera, sin necesidad de conocer nada más allá de lo que ya utilizaba el primer agente.
  • Qué tanto hay que mantener: Para ajustar el comportamiento de la herramienta de búsqueda, agregar una política de intentos repetidos o modificar el tiempo de espera, es necesario editar directamente LookupToolClient, y cada agente que dependa de él adoptará automáticamente el cambio. MCP no elimina esa carga de mantenimiento, pero sí la consolida: ahora el comportamiento de cada herramienta se encuentra detrás de las mismas dos funciones, list_tools y call_tool, en lugar de estar disperso en tantas formas distintas de clientes como herramientas existen.
  • Nada de esto simplifica la lógica real de la herramienta. perform_lookup y fetch_record_from_db siguen necesitando ser escritos y mantenerse funcionales de cualquier manera. Lo que cambia es todo lo relacionado con esa lógica: cómo se descubre, cómo se invoca, cómo se manifiestan los fallos y cuánta carga debe asumir el propio código del agente en comparación con cuando se maneja automáticamente al seguir un protocolo compartido.

    Integrar una herramienta MCP en el sistema LangGraph (continuación de la Parte 2)

    Hasta ahora, cada ejemplo se ha centrado en un único agente que llama a una sola herramienta de forma aislada. Vale la pena cerrar esa brecha incorporando nuevamente una herramienta basada en MCP al grafo multiagente creado en la Parte 2, ya que allí es donde realmente convergen los temas de los tres artículos de esta serie.

    Recuerde el gráfico de la Parte 2: un paso de entrada, la interpretación por parte del LLM, el motor de reglas presentado en la Parte 1, y el enrutamiento condicional que dirige el flujo ya sea a un nodo de notificación o a revisión humana. Imagine que ahora el motor de reglas depende de una verificación en algún sistema externo antes de poder tomar una decisión; ese tipo de dependencia habría significado anteriormente la creación de un cliente ad hoc dedicado, similar al que se construyó antes en este artículo, conectado directamente a dicho nodo.

    Con un servidor MCP que expone esa misma función de búsqueda como herramienta, las responsabilidades del nodo apenas cambian. Sigue leyendo desde el estado del gráfico y sigue escribiendo su decisión de vuelta en ese estado; solo accede al sistema externo llamando a session.call_tool en lugar de utilizar un cliente dedicado:

    async def rules_engine_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            lookup_result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["category"]}
            )
        decision = evaluate_rules(state["extracted_fields"], lookup_result)
        return {"decision": decision["decision"], "decision_reason": decision["reason"]}
    

    La posición del nodo dentro del flujo general no se ve afectada por nada de esto. Sigue estando en el mismo lugar, aguas abajo de llm_interpretation y aguas arriba del ramal condicional que ya existía en la Parte 2, aún limitado por el alcance de permisos definido en la discusión sobre normas de ese artículo, y sigue registrándose bajo el mismo ID de trazabilidad descrito en su sección sobre observabilidad. El único cambio real está en la forma en que el nodo comunica con el mundo exterior: en lugar de depender de un cliente creado específicamente para este nodo, ahora realiza una llamada a una herramienta que cualquier otro nodo, ya sea en este grafo o en uno futuro, podría invocar a través de esa misma ruta.

    Aquí es donde realmente se unen los tres componentes de la serie. Un agente que solo toma decisiones cuando se le permite explícitamente hacerlo, operando dentro de un grafo que lo coordina junto con otros agentes, y accediendo a sistemas externos a través de la misma interfaz estandarizada que comparten todos los agentes. Ninguno de estos tres elementos —el motor de reglas, el grafo o MCP— necesita conocer en detalle a los otros dos. Solo deben seguir la misma disciplina que esta serie ha destacado constantemente: responsabilidades definidas, contratos explícitos y nada dejado a la suposición.

    Costo y latencia: cuánto cuesta realmente la orquestación

    Cada capa que se añade a lo largo de esta serie aporta cierta cantidad de estructura, y toda esa estructura no es gratuita. En lugar de dejar oculto el costo, resulta útil rastrear qué ocurre realmente con el tiempo de respuesta una vez que una solicitud pasa por todas las etapas descritas hasta ahora.

    Considere una única solicitud que se mueve a través del grafo presentado en la Parte 2, ahora ampliado con la búsqueda basada en MCP mencionada anteriormente. El camino se desglosa más o menos de la siguiente manera: la etapa de recepción realiza un formato ligero sin llamadas salientes, por lo que su costo es de unos pocos milisegundos como máximo. La etapa de interpretación del LLM llama a un modelo para extraer campos estructurados de la solicitud en bruto, y esta suele ser el costo más elevado de todo el proceso, añadiendo a menudo varios cientos de milisegundos dependiendo de la elección del modelo y de la longitud del prompt. La llamada que realiza el motor de reglas a la herramienta de búsqueda mediante MCP implica un viaje de ida y vuelta por red, además del tiempo que necesita la propia herramienta para responder; se trata de un costo significativo, pero generalmente menor que el de la etapa del LLM. La evaluación de las reglas en sí, al tratarse simplemente de lógica determinística, es prácticamente gratuita. La etapa de enrutamiento y la notificación final añaden solo una pequeña cantidad adicional a ese costo.

    Si sumamos estas cifras, la imagen se vuelve clara: una solicitud que realiza solo un viaje al modelo y una llamada a una herramienta tiene su tiempo total determinado casi en su totalidad por esas dos operaciones, mientras que la coordinación propia del grafo apenas se nota. Los nodos, aristas y estado compartido introducidos en la Parte 2 proporcionan estructura sin imponer una latencia real por sí mismos: leer y escribir un objeto de estado compartido es algo sencillo. Lo que realmente consume tiempo es acceder a un modelo o a un sistema externo.

    Eso cambia la forma en que se debe pensar sobre el ajuste del rendimiento. Agregar más nodos, más ramas de enrutamiento o más verificaciones de seguridad a un grafo apenas afecta la velocidad, ya que se trata simplemente de llamadas a funciones y búsquedas en un diccionario. Lo que realmente ralentiza al sistema son cada llamada a un LLM y cada llamada a una herramienta externa que se encuentra en la ruta crítica de la solicitud. Un sistema construido con tres agentes que llaman a un modelo uno tras otro funcionará notablemente más lento que uno que llama al modelo una sola vez, sin importar cuán eficiente sea la orquestación circundante.

    La conclusión práctica es sencilla: cuando la latencia es importante en un flujo de trabajo, no comience examinando la forma del gráfico. Empiece por contar las invocaciones al modelo y las llamadas a herramientas externas que una solicitud normal debe realizar antes de finalizar, y pregúntese si alguna de ellas puede ejecutarse en paralelo en lugar de secuencialmente, o si se pueden omitir por completo en las solicitudes que no las necesitan.

    Lo que MCP no resuelve

    Vale la pena ser igualmente franco sobre los límites de MCP, al igual que lo fue con sus ventajas mencionadas anteriormente, ya que la mayoría de los artículos sobre el tema se centran en las ventajas y rara vez abordan sus limitaciones. Las mismas tres áreas discutidas allí —descubrimiento, invocación y manejo de errores— merecen ser analizadas desde un ángulo diferente.

    • La estandarización del descubrimiento y la invocación no soluciona un herramienta mal diseñada: MCP estandariza cómo se localiza y llama a una herramienta, no lo que ocurre dentro de ella. Una herramienta con una implementación lenta, inestable o mal estructurada sigue siendo lenta, inestable y mal estructurada incluso cuando se encuentra detrás de un servidor MCP. El protocolo traslada la inconsistencia, pasándola del código que realiza la llamada a la propia implementación de la herramienta, pero no hace que esa inconsistencia desaparezca.
  • Una forma uniforme de error no es lo mismo que un manejo adecuado de errores: Las fallas siguen ocurriendo, las herramientas siguen agotando su tiempo de ejecución y los sistemas externos siguen dejando de funcionar. MCP proporciona a esas fallas una forma predecible y consistente cuando ocurren, pero el código que las llama sigue siendo responsable de decidir qué hacer a continuación: si reintentar, recurrir a una solución alternativa o propagar el error hacia arriba, tal como lo era antes. Un formato de error consistente no equivale a que la falla se esté gestionando realmente.
  • Ejecutar el protocolo conlleva sus propios costos operativos: Un servidor MCP es un proceso adicional que hay que ejecutar, desplegar y mantener en buen estado. Para un único agente que llama a una herramienta simple, eso representa un costo real, y el enfoque de cliente ad hoc descrito anteriormente en la serie habría sido más rápido de configurar y más fácil de entender. Este costo adicional se ve agravado por la juventud del ecosistema: las herramientas, el soporte para depurar y las convenciones establecidas todavía están rezagadas en comparación con algo tan maduro como una API REST tradicional. En la práctica, eso significa pasar más tiempo leyendo directamente el código fuente y las especificaciones, con menos patrones probados de los que poder valerse.
  • Nada de esto se opone a la adopción de MCP. Es un recordatorio de que elegir un protocolo no sustituye el trabajo de ingeniería que aún debe realizarse en su base. El cambio que aporta MCP es real, como se ha demostrado en las secciones anteriores, pero es más limitado que la idea general de “agentes conectándose a herramientas”, y vale la pena ser precisos sobre dónde se sitúa exactamente ese límite antes de decidir si su adopción tiene sentido, algo de lo que trata la siguiente sección.

    Cuándo vale la pena adoptar MCP

    Dadas las consideraciones presentadas anteriormente, no existe una respuesta generalizada: ni un claro “siempre hay que adoptarlo” ni “nunca molestarse”. Lo importante es verificar un conjunto concreto de condiciones antes de implementarlo en cualquier sistema dado.

    • Más de un agente necesitará las mismas herramientas. Casi todos los beneficios descritos anteriormente provienen del reuso: un segundo agente puede conectarse a un servidor ya creado, en lugar de duplicar un cliente o crear uno posteriormente. Cuando solo hay un agente y una herramienta, ese beneficio simplemente aún no existe; no hay nada que compartir, por lo que el enfoque ad hoc mencionado antes sigue siendo la opción más sencilla para ese caso específico.
    • Se espera que aumente la cantidad de herramientas. Una interfaz estandarizada cobra más valor a medida que hay más herramientas detrás de ella. Con dos o tres herramientas, ejecutar un servidor dedicado podría no valer la pena debido a los costos adicionales. Una vez que se manejan diez o veinte herramientas —cada una de las cuales necesitaría un cliente personalizado por separado—, la carga de mantenimiento del enfoque ad hoc comienza a convertirse en un verdadero problema.
  • El sistema debe ser capaz de sobrevivir más allá de su primer lanzamiento. Una de las ventajas que ofrece MCP es que la adición de un nuevo agente o una nueva herramienta con el tiempo no obliga a volver a configurar todo lo que ya está en funcionamiento. Ese beneficio se acumula a lo largo de la vida útil del sistema, y es fácil pasarlo por alto si solo se considera el costo de la configuración inicial.
  • Por otro lado, esto no es adecuado para un único agente que se comunica con una herramienta estable y bien comprendida que no va a cambiar. En ese escenario, crear un cliente ad hoc es más rápido, se reduce en uno el componente que hay que mantener, y la coordinación que ofrece MCP no cuenta con otro consumidor que pueda utilizarla realmente.

    La prueba práctica que se puede aplicar aquí es similar a la utilizada anteriormente para el propio motor de reglas: ¿añadir esta complejidad resuelve un problema con el que realmente se enfrenta este sistema en su etapa actual, o se adopta simplemente porque es la solución de moda para un problema que el sistema aún no tiene?

    Conclusión: Cierre de la serie

    A lo largo de tres artículos, un sistema se desarrolló paso a paso, con cada capa basada en la anterior. La primera parte creó un único agente lo suficientemente confiable como para que se le asignara una decisión real, al separar estrictamente la interpretación del acto de decidir. La segunda parte le dio a ese agente compañeros, reuniendo varios agentes especializados dentro de un grafo que compartía estado entre ellos, protegido por permisos explícitos y con seguimiento end to end para que cada solicitud pudiera rastrearse desde el momento en que entraba hasta el momento en que salía. Este artículo cerró la brecha restante: cómo esos agentes pueden ir más allá del grafo, al estandarizar ese acceso a través de MCP en las situaciones en las que realmente resulta útil, al tiempo que se señalaba claramente dónde no lo es.

    None of these three pieces is especially complex on its own. What actually makes the resulting system dependable is one habit, repeated at every layer without exception: responsibilities stay narrow, contracts stay explicit, and nothing is left to guesswork or allowed to happen quietly in the background. That habit is the real subject of this series, more than any particular tool—LangGraph and MCP just happened to be the frameworks used to put it into practice, but the underlying principles hold regardless of which tools you reach for.