Saltar al contenido principal

LangChain Acaba de Hacer MCP de Primera Clase — Lo Ejecutamos el Mismo Día, y Tres Cosas Todavía No Funcionan

· 11 min de lectura
Rafael Fernandes
NLP Engineer & Tech Writer at WiLine
Share:
Frameworks de Agentes · Noticias de IAEl soporte de MCP llega al paquete principal de LangChain

En julio la especificación de MCP arrancó las sesiones. Escribimos entonces que el cambio era infraestructura, no changelog — que un núcleo sin estado dejaría a los servidores sentarse detrás de balanceadores de carga corrientes y sobrevivir a los redespliegues, y que los clientes tendrían que ponerse al día.

Hoy LangChain se puso al día. El soporte de MCP salió del paquete separado langchain-mcp-adapters y entró en langchain mismo, reconstruido sobre FastMCP, con dos funciones que la especificación vieja hacía imposibles: elicitación como interrupt de LangGraph y un catálogo de herramientas cacheable.

Lo instalamos esa misma tarde y lo apuntamos a un servidor que escribimos contra la nueva especificación. El anuncio es correcto. El ecosistema alrededor no está listo — y uno de los huecos convierte silenciosamente una puerta de aprobación humana en una frase que el modelo se inventa.

Lo que realmente salió​

pip install "langchain[mcp]", que requiere 1.4.0 o superior, y en beta — lo dice al importar, como puedes ver en nuestras capturas más abajo. Python hoy, TypeScript "poco después".

  • Una clase, en el paquete principal. MultiServerMCPClient se colapsa en MCPAdapter. async with MCPAdapter(url) as adapter: y luego await adapter.list_tools(), y lo que vuelve son herramientas de LangChain corrientes que van a donde van las herramientas.
  • Construido sobre FastMCP, así que transportes, autenticación (bearer, OAuth 2.1, máquina a máquina, CIMD, o cualquier httpx2.Auth), gestión de conexiones y negociación de protocolo vienen del cliente de debajo. MCP tiene ahora dos eras — el protocolo con handshake del 2025-11-25 y el sin estado del 2026-07-28 — y el cliente de FastMCP elige una por conexión: "prueba el protocolo nuevo y recae en el handshake para un servidor que no se ha actualizado".
  • Elicitación vía interrupts. Cuando una herramienta del servidor MCP no puede terminar sin preguntarle a un humano, la ejecución de tu agente se pausa como un interrupt() de LangGraph, y la reanudas con una respuesta estructurada. Esto solo es posible porque la especificación sin estado convirtió una pregunta a mitad de llamada en una ronda reintentable en lugar de algo mantenido abierto en un socket.
  • Caché del lado del cliente. fastmcp.Client(url, cache=True) respeta las pistas de frescura del servidor para que el catálogo de herramientas no se vuelva a pedir en cada ejecución.
  • ClientGroup para varios servidores a la vez, cada uno conservando su propia era y sus credenciales, con los nombres de herramienta prefijados por servidor — billing_search y docs_search se mantienen distintos.

El encuadre del anuncio es el mismo que usamos en julio: "un redespliegue ya no mata sesiones vivas, porque no hay ninguna."

Lo que la especificación sin estado realmente quitó​

Una llamada a una herramienta, antes y después, es la imagen más clara de lo que cambió — así que aquí está, con un tercer panel para lo que realmente obtuvimos:

Tres diagramas de secuencia comparando una llamada a herramienta. Bajo la era del handshake del 2025-11-25 el agente envía initialize, recibe un session id, y luego envía tools/call con ese id. Bajo la especificación sin estado del 2026-07-28 no hay handshake y el agente envía tools/call directamente. Contra un servidor FastMCP por defecto en la nueva especificación, la misma petición es rechazada con Bad Request: Missing session ID.

Los dos primeros paneles son la promesa, y la promesa es real. El tercero es lo que hace un servidor recién creado la tarde en que sale el cliente.

Tres cosas que todavía no funcionan​

Construimos un servidor MCP que expone una pequeña base de datos de citas y facturas, apuntamos un agente de LangChain hacia él, y pusimos un reembolso detrás de un humano. (El modelo del propio agente corre sobre un gateway autoalojado — eso está entre el agente y el LLM, no entre el agente y MCP.) Funciona — ese es el tutorial. Llegar hasta ahí sacó a la luz tres huecos entre lo que está escrito y lo que corre.

1. La especificación es sin estado. Tu servidor no, por defecto.​

La primera petición a un servidor FastMCP 4.0.2 recién creado — aquí un curl simple pidiendo tools/list, para que nada del lado del cliente cargue con la culpa:

Un POST con curl al endpoint MCP devolviendo Bad Request: Missing session ID con el código de error JSON-RPC -32600

Un error sobre un concepto que la especificación borró hace cinco semanas. El cliente es sin estado; el servidor sigue usando por defecto el transporte con estado.

El arreglo no es una cabecera, ni una opción del cliente, ni nada que tú envíes. Es un argumento en la llamada que arranca tu servidor — la última línea del archivo del servidor, donde mcp es tu instancia de FastMCP:

office_tools.py
from fastmcp import FastMCP

mcp = FastMCP("office-tools")

# ... tus funciones @mcp.tool ...

if __name__ == "__main__":
# Sin stateless_http=True este servidor le responde a un cliente
# del 2026-07-28 con "Bad Request: Missing session ID".
mcp.run(transport="http", host="127.0.0.1", port=8770, stateless_http=True)

Nada en el anuncio lo dice — con razón, porque el post trata del cliente. Pero significa que la mitad cliente de la actualización es un pip install, y la mitad servidor es un flag que tienes que saber que existe.

Existen dos flags, y run_http_async (al que mcp.run llama para HTTP) acepta ambos. De su propio docstring:

stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
stateless: Alias for stateless_http for CLI consistency

Un interruptor, dos nombres, y el ajuste al que recae está apagado.

2. ctx.elicit es la API vieja — y el error va al modelo, no a ti​

Todos los ejemplos de elicitación que encontrarás llaman a await ctx.elicit(message, response_type) dentro del cuerpo de la herramienta, donde ctx es el objeto Context que FastMCP le pasa a tu herramienta. Ese es el mecanismo de la era del handshake: bloquea a mitad de ejecución y habla por el canal de retorno de la sesión — el canal de retorno que una conexión sin estado no tiene. La documentación de FastMCP es explícita en que es para conexiones ≤ 2025-11-25, y promete que llamarlo en una moderna "lanza un error de era claro en lugar de fallar de forma oscura".

Sí lanza uno. Ese no es el problema. Reconstruimos la herramienta de reembolso alrededor de ctx.elicit y ejecutamos el mismo agente contra ella:

La ejecución del agente imprimiendo interrupt raised? False, tres mensajes de herramienta donde issue_refund tiene status=error llevando 'elicitation via server-initiated requests is unavailable on 2026-07-28 connections', el modelo respondiendo que ha iniciado el reembolso y que un humano debe aprobarlo, y una consulta sqlite3 mostrando la factura 2 todavía abierta

Léelo en orden. FastMCP lanzó el error de era, exactamente como está documentado. LangChain lo capturó y lo convirtió en un ToolMessage con status="error". Eso es deliberado: langchain/mcp/tools.py conecta un manejador cuyo docstring dice que existe para entregarle al modelo el detalle del error del servidor "en lugar de terminar la ejecución". El modelo leyó ese error y le dijo al usuario:

He localizado a Maria Alvarez y he identificado su factura abierta (ID: 2) por 80,00 $. He iniciado la solicitud de reembolso para esta factura. Ten en cuenta que un humano debe aprobar el importe y dar una razón para completar el reembolso.

No se lanzó ningún interrupt. El proceso salió con 0. Nada está pendiente, nada está esperando a un humano, y a nadie se le va a preguntar nunca — y la factura sigue open, así que el reembolso tampoco ocurrió. Lo que obtienes no es una acción destructiva colándose por una puerta; es un flujo de aprobación que silenciosamente no existe, descrito en un español fluido por un modelo que leyó el error y lo parafraseó como progreso.

El arreglo es que el patrón moderno hace que la herramienta devuelva un InputRequiredResult describiendo lo que necesita, y salga. Tu agente lo expone como el interrupt de LangGraph, un humano responde, y el cliente MCP reemite el mismo tools/call con la respuesta adjunta. Pero el modo de fallo es la historia: un error de era es algo estupendo que lanzarle a un programa, y algo terrible que entregarle a un modelo de lenguaje al que se premia por sonar servicial.

3. cache=True es necesario, no suficiente​

La caché del cliente respeta las pistas ttlMs y cacheScope que un servidor adjunta a su respuesta de tools/list, y solo contra servidores de la era moderna que las envían. Un servidor FastMCP por defecto no envía ninguna — volcamos nuestra propia respuesta de tools/list y no lleva pistas de caché en absoluto. Así que enciendes la caché, llamas a list_tools(cache_mode="use") dos veces seguidas, cronometras ambas, y obtienes:

Dos descubrimientos de cinco herramientas cada uno, cronometrados en 14,6 ms y 12,5 ms, sin efecto de caché

Concluyes que la caché está rota. No lo está — no había nada que cachear. Vale la pena notar que la caché pertenece al fastmcp.Client, no a MCPAdapter, y que un cliente por llamante evita que los catálogos se crucen entre inquilinos.

Y dos más pequeñas, en el propio anuncio​

El fragmento de elicitación lanza AttributeError. El post lee el interrupt como paused["__interrupt__"][0].value.requests[0] — acceso por atributo. En langchain/mcp/elicitation.py en 1.4.0:

class MCPElicitationInterrupt(TypedDict):
type: Literal["mcp_elicitation"]
tool_name: str
requests: list[MCPElicitationRequest]

Un TypedDict es un dict en tiempo de ejecución, así que .requests no resuelve. Es value["requests"] — que el propio fragmento acierta unas líneas después, leyendo question["key"] por subíndice.

El enlace a la documentación de elicitación da 404. El post cierra la sección apuntando a docs.langchain.com/oss/python/langchain/mcp/elicitation para "declinar una pregunta, y poner herramientas destructivas detrás del mismo flujo de aprobación". Esa página devuelve 404 al momento de publicar, mientras que la página padre y sus otras hijas — mcp, mcp/connections, mcp/tools, mcp/auth — resuelven todas. Es, inconvenientemente, la única página que habría documentado el flujo de aprobación que el hueco 2 muestra cayéndose.

Por qué esto te importa a ti, específicamente​

Si consumes servidores MCP que ejecuta otra persona, esto son buenas noticias sin más y notarás sobre todo la ruta de import más corta.

Si escribes servidores MCP, la revisión de julio movió el suelo y las herramientas todavía se están asentando. Tres cosas son ahora tuyas para hacerlas bien: tu servidor no se vuelve sin estado porque la especificación lo hiciera — pones un flag; una herramienta que necesita entrada humana tiene que escribirse para ser reentrada en lugar de reanudada — interrupt() desenrolla la llamada completa, así que el cuerpo de la herramienta se ejecuta otra vez desde arriba cuando respondes; y cualquier ejemplo de elicitación anterior a agosto te está enseñando una API cuyo fallo aterriza en el contexto del modelo en lugar de en tus logs.

Eso último se generaliza más allá de MCP. A medida que los frameworks mejoran en mantener vivos a los agentes a través de los errores, la clase de bug que termina una ejecución se encoge y la clase que se le narra a un usuario crece. Una puerta que falla cerrada es un bug que encuentras en pruebas. Una puerta que nunca se instaló, descrita por un modelo como pendiente de tu aprobación, es una que encuentras en una auditoría.

La dirección es la correcta. El núcleo sin estado es lo que hace que la pausa de aprobación humana de un agente sobreviva a un redespliegue, y eso es una capacidad real más que una refactorización. Solo no esperes que tu primera petición funcione.


📖 Fuentes: LangChain — MCP en LangChain: protocolo sin estado, elicitación y más · Documentación de MCP en LangChain · Migrar desde langchain-mcp-adapters · Documentación del cliente FastMCP · Elicitación en FastMCP · Especificación MCP 2026-07-28

Versiones probadas: langchain 1.4.0, fastmcp 4.0.2, mcp 2.1.1, modelo servido sobre un gateway autoalojado. La captura de ctx.elicit es un render de la salida real de esa ejecución, no una captura de pantalla.

Comments & questions

Hit an error, spotted a typo, or have a question? Leave a note below.