Hace unas semanas, alguien del equipo de datos preguntó si podíamos actualizar el esquema de la base de datos que estaba siendo rellenado por una de las herramientas de nuestro complejo sistema agente. La actualización es simple: se agregan dos nuevas columnas a la tabla.
La definición de la herramienta vivía en el orquestador del agente. Una segunda versión similar vivía en el agente de validación. Una tercera versión ligeramente diferente y desactualizada se encontraba en un módulo de utilidad que alguien había escrito hace tres sprints. La lógica de aprobación humana en el circuito se conectó directamente a los bordes del gráfico, una implementación personalizada por herramienta. Cambiar el esquema significó tocar cuatro archivos, volver a probar cada agente por separado y esperar que nada posterior se rompiera silenciosamente.
Lo arreglamos pero surgió una pregunta seria: ¿por qué lo construimos de esta manera?
La respuesta honesta es que no teníamos otra alternativa. La llamada de herramientas en LangGraph es una preocupación local por diseño. Usted define las herramientas donde las necesita, las llama donde las llama y es dueño de toda la plomería. Esto es manejable cuando solo se tienen dos agentes, pero se convierte en un problema cuando siete agentes comparten herramientas superpuestas con una puerta humana.
Después de investigar un poco, decidimos que en lugar de definir herramientas localmente para cada agente, deberíamos usar un recurso compartido que pueda alojar todas nuestras herramientas y que cualquier agente pueda usarlas.
En este artículo
¿Qué es MCP? Construyendo el servidor MCP Stdio vs HTTP Conectándolo a LangGraph Human-in-the-loop en el límite del protocolo ¿Qué puede interrumpirse en la producción y por qué? Impacto de MCP en nuestro sistema agente Conclusión
¿Qué es MCP?
El Model Context Protocol es un estándar abierto publicado por Anthropic a finales de 2024. Estandariza cómo un agente de IA descubre y llama a las herramientas. En lugar de definir herramientas dentro del orquestador, las ejecuta en un servidor independiente. El agente se conecta a ese servidor en tiempo de ejecución, pregunta qué herramientas están disponibles y obtiene una lista.
Un ingeniero senior que lea este artículo preguntará inmediatamente: ¿no podría simplemente crear un registro de herramientas centralizado e inyectarlo en cada agente al inicio? Me pregunté esto a mí mismo y utilicé el registro de herramientas en lugar de MCP en otro sistema.
Sí, podrías, y si ya tienes algo así funcionando, MCP no es una emergencia. Lo que un registro personalizado no ofrece es el límite de interoperabilidad. MCP es un protocolo, no una biblioteca. Cualquier cliente compatible con MCP puede conectarse a su servidor, LangGraph hoy, un marco diferente el próximo año. Un cliente TypeScript puede llamar a su servidor Python sin ningún trabajo de integración adicional. Un registro de herramientas no proporciona esta funcionalidad.
También hay un punto de propiedad del equipo. En nuestro caso, el equipo de ML era dueño de las herramientas, el equipo de aplicaciones era dueño del gráfico. MCP les dio un contrato limpio sin un código base compartido.
Construyendo el servidor MCP
Un servidor MCP puede exponer tres cosas: herramientas (acciones invocables), recursos (datos de solo lectura) y mensajes (plantillas reutilizables). Para un sistema agente que necesita tomar algunas acciones, las herramientas son la principal preocupación.
El SDK de Python se entrega con FastMCP, que maneja la generación de esquemas a partir de sugerencias de tipo y administra el ciclo de vida del protocolo. Tienes que escribir una función y decorarla con una herramienta decoradora y el servidor se encarga del resto.
Una cosa que sorprende a la gente con el transporte stdio: nunca escriba en stdout. El protocolo MCP utiliza stdout como canal de comunicación. Cualquier llamada perdida a print() corromperá el flujo de mensajes de maneras que resultan muy confusas de depurar.
importar sys importar registro desde mcp.server.fastmcp importar FastMCP logging.basicConfig(level=logging.INFO, stream=sys.stderr) logger = logging.getLogger(“analyst-tools”) mcp = FastMCP(“analyst-tools”) @mcp.tool() async def run_analysis(code: str, dataset: str) -> dict: “”” Ejecuta Python fragmento contra datos en vivo y devuelve el resultado. Se usa cuando el usuario desea calcular agregados, filtrar registros o derivar información. El código debe asignar su salida final a una variable denominada ‘salida’. Args: código: conjunto de datos de Python: uno de ‘ventas’, ‘inventario’, ‘pipeline’.info(f”run_analysis | dataset={dataset}”) return await generate_in_sandbox(code, conjunto de datos) @mcp.tool() async def write_to_db(table: str, payload: dict) -> dict: “”” Persiste un registro de resultados en la tabla de resultados del analista. Llame a esto solo después de que run_analysis haya devuelto un resultado verificado. Args: tabla: nombre de la tabla de destino. carga útil: pares clave-valor para escribir como un nuevo registro. “”” logger.info(f”write_to_db | table={table}”) return await persist_result(tabla, carga útil) if __name__ == “__main__”: mcp.run(transport=”stdio”)
El LLM utiliza las cadenas de documentación para ayudar al agente a decidir a qué herramienta llamar. Por eso, escribir una buena cadena de documentación es muy importante.
Estándar frente a HTTP
Esta decisión surge en cada implementación de producción y la mayoría de los artículos la omiten.
Stdio ejecuta el servidor como un subproceso del cliente. La comunicación se produce a través de entrada y salida estándar. La latencia es de milisegundos de un solo dígito, no hay red involucrada y la configuración es mínima. La elección correcta para el desarrollo local, implementaciones en una sola máquina o en cualquier lugar donde el servidor y el cliente vivan en el mismo árbol de procesos.
Streamable HTTP ejecuta el servidor como un servicio independiente. Úselo cuando el servidor necesite compartirse entre varios clientes o máquinas, cuando desee implementarlo como un contenedor o cuando necesite escalamiento horizontal. Las implementaciones sin servidor como Cloud Run funcionan bien aquí. Stdio no se ajusta en absoluto al modelo sin servidor porque supone un proceso principal de larga duración.
Cambiar entre estos en FastMCP es solo una línea:
mcp.run(transport=”streamable-http”, host=”0.0.0.0″, puerto=8080)
Sólo tenemos que cambiar el transporte en mcp.run() y todo lo demás sigue igual.
Para los requisitos de residencia de datos, un servidor MCP que se ejecuta localmente con herramientas que nunca tocan una API externa le brinda una historia clara para su equipo de cumplimiento. Al protocolo no le importa dónde se ejecuta el servidor.
Conectándolo a LangGraph
La biblioteca langchain-mcp-adapters gestiona el ciclo de vida del subproceso, realiza el protocolo de enlace de descubrimiento de herramientas y traduce los esquemas de herramientas MCP en objetos de herramientas compatibles con LangChain.
de langchain_mcp_adapters.client importar MultiServerMCPClient de langgraph.graph importar StateGraph, MessagesState, INICIO de langgraph.prebuilt importar ToolNode, herramientas_condición de langchain_google_vertexai importar ChatVertexAI llm = ChatVertexAI( model=”gemini-2.5-flash”, temperatura=0, max_tokens=None ) async def run(consulta: str): async con MultiServerMCPClient({ “analyst-tools”: { “command”: “python”, “args”: [“./mcp_server.py”]”transport”: “stdio”, } }) como cliente: herramientas = await client.get_tools() llm_with_tools = llm.bind_tools(herramientas) def agent_node(estado: MessagesState): return {“mensajes”: [llm_with_tools.invoke(state[“messages”])]} gráfico = StateGraph(MessagesState) Graph.add_node(“agente”, agente_nodo) Graph.add_node(“herramientas”, ToolNode(herramientas)) Graph.add_edge(INICIO, “agente”) Graph.add_conditional_edges(“agente”, herramientas_condición) Graph.add_edge(“herramientas”, “agente”) aplicación = Graph.compile() resultado = espere app.ainvoke({ “mensajes”: [{“role”: “user”, “content”: query}]
}) imprimir(resultado[“messages”][-1].contenido)
tools_condition es un módulo LangGraph incorporado que verifica si el último mensaje contiene llamadas a herramientas o no. En caso afirmativo, diríjase al ejecutor de la herramienta y, en caso contrario, terminamos. Usarlo en lugar de escribir su propia función de enrutamiento es importante porque maneja casos extremos y errores de implementación.
Un comportamiento que vale la pena conocer: MultiServerMCPClient crea una nueva sesión MCP por llamada de herramienta de forma predeterminada. Para una sola solicitud que realiza cinco llamadas secuenciales a herramientas, son cinco apretones de manos. Está bien para stdio en la misma máquina, pero se nota en el transporte HTTP con un servidor remoto. Para cargas de trabajo de producción con llamadas a herramientas encadenadas, utilice async con client.session(“analyst-tools”) para fijar varias llamadas a una sesión.
Human-in-the-Loop en el límite del protocolo
Antes de MCP, nuestra puerta de aprobación vivía en el gráfico. Usamos interrupt_before en nodos específicos, conectamos lógica de confirmación personalizada en los bordes del gráfico y actualizamos la interfaz de usuario cada vez que se agregaba una nueva herramienta confidencial. Funcionó, pero también significó que agregar una herramienta que requería aprobación era un ejercicio de coordinación de tres equipos.
Después de MCP, la puerta se mueve a una sola capa entre el ejecutor LangGraph y el cliente MCP. Cualquier herramienta que coincida con la política de sensibilidad llega a la puerta antes de llegar al servidor. El gráfico no tiene conocimiento de ello.
SENSITIVE_TOOLS = frozenset({“write_to_db”, “send_notification”, “trigger_webhook”}) async def gated_call(nombre_herramienta: str, argumentos: dict, ejecutar) -> dict: if nombre_herramienta en SENSITIVE_TOOLS: # En producción: enviar a Slack/UI interna/cola de auditoría print(f”\nAPROBACIÓN REQUERIDA {nombre_herramienta}”) print(f”Argumentos: {argumentos}”) decisión = input(“¿Aprobar? (s/n): “).strip().lower() if decisión != “y”: return { “status”: “rechazado”, “motivo”: f”El operador rechazó ‘{tool_name}'”. } return await ejecutar(nombre_herramienta, argumentos)
SENSITIVE_TOOLS es un conjunto único, consultado para cada llamada de herramienta independientemente del agente que la activó. ¿Nueva herramienta sensible agregada al servidor? Agregue el nombre a este conjunto. El gráfico no cambia. La interfaz de usuario de aprobación no cambia. En nuestro sistema interno cargamos esto desde un archivo de configuración al inicio. El equipo de producto y cumplimiento podría actualizarlo sin implementar código.
¿Qué puede fallar en la producción y por qué?
El servidor falla a mitad de la ejecución. El cliente recibirá un error en la próxima llamada a la herramienta. ToolNode de LangGraph muestra esto al LLM como un mensaje de error de herramienta. Si el modelo se recupera o se genera confusión depende del indicador del sistema. Como mínimo, registre el subproceso stderr por separado para que pueda ver qué mató al servidor, sin que la depuración sea una conjetura.
El LLM llama a la herramienta equivocada. MCP no te protege de esto. Si las descripciones de sus herramientas son vagas o tienen significados superpuestos, el modelo tomará la decisión de enrutamiento equivocada. Pasamos un tiempo considerable ajustando las cadenas de documentación en nuestro servidor específicamente porque una descripción mal redactada hacía que se llamara a write_to_db antes de que terminara run_analysis. Trate las descripciones de herramientas como un problema de ingeniería inmediato.
Puerta de aprobación en flujos de trabajo de larga duración. Si un humano necesita aprobar una llamada de herramienta y tarda cinco minutos, el gráfico del agente se suspende en espera. LangGraph admite el estado persistente del gráfico mediante puntos de control, por lo que puede dejar que el proceso salga y se reanude cuando llegue la decisión. Esto es más complicado que lo que se muestra aquí, pero es la arquitectura adecuada para flujos de trabajo que no pueden bloquear un hilo indefinidamente.
Impacto de MCP en nuestro sistema agente
Migramos siete herramientas en el servidor, tres de ellas están sujetas a aprobación. El orquestador que los llama no tiene conocimiento de lo que hacen.
Eliminamos por completo la duplicación de herramientas. Ahora, run_analysis se define exactamente en un lugar y atiende siete flujos de trabajo simultáneamente. Para actualizar el esquema de salida, solo tenemos que realizar cambios en el servidor y luego cada consumidor aceptará el cambio.
Agregar nuevas capacidades se volvió rápido. Por ejemplo, agregamos una herramienta generate_visualisation la semana siguiente y el agente la estaba usando al día siguiente. No se realizan cambios de orquestador.
Terminamos con un equipo dueño de las herramientas, otro dueño del gráfico y un contrato claro entre ellos. Cuando el equipo de analistas quiere una nueva capacidad, habla con el equipo de ML sobre el servidor, no con el equipo de aplicaciones ni con el equipo de gráficos.
Quiero compartir una cosa que MCP no soluciona: no hará que las herramientas poco confiables sean confiables. No ayudará al LLM a tomar mejores decisiones de ruta si sus descripciones son malas. Y no reemplaza la observabilidad; aún necesita registrar las llamadas a las herramientas y rastrear las rutas de ejecución. La estructura hace que sea más fácil instrumentarlos, pero el trabajo sigue siendo suyo.
Conclusión
Al hacer la transición a MCP y trasladar herramientas de nuestro orquestador de agentes local a un servidor dedicado, limpiamos nuestra base de código, desacoplamos nuestras restricciones de ingeniería e hicimos que todo el sistema de agentes fuera fácil de implementar.
Debido a esta transición, nuestro equipo de ML ahora puede implementar y versionar herramientas de forma independiente sin tocar el gráfico de la aplicación.
Si disfrutó de este análisis profundo de MCP, le invito a que consulte mi serie en curso: RAG para la base de conocimientos empresariales en búsqueda híbrida y reclasificación en RAG de producción.