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

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.
MultiServerMCPClientse colapsa enMCPAdapter.async with MCPAdapter(url) as adapter:y luegoawait 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. ClientGrouppara varios servidores a la vez, cada uno conservando su propia era y sus credenciales, con los nombres de herramienta prefijados por servidor —billing_searchydocs_searchse 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:
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 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:
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:

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:

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.
