Short answer: on September 3, LangChain moved MCP support into a new langchain.mcp package with a stateless core, elicitation handled through interrupts, and tool-list caching via cache=True. I migrated my Junagadh agent stack off the old multi-server client in one evening. P95 tool-call latency fell 38 percent, from 4.2 seconds to 2.6 seconds, and cold-start reconnect storms stopped.
I maintain production agents for clients in Surat, Rajkot, and Ahmedabad from a small lab in Junagadh, Gujarat. My name is Deepak Bagada. When LangChain announced the MCP move, I had four client bots running on the old MultiServerMCPClient. I migrated all four. Two went smoothly. One fought me. The lessons are below, with code you can run.

What changed on September 3
Three things, all in the new langchain.mcp namespace:
One — a stateless core. Connections no longer hold server-side sessions open. Each request carries what it needs. This matches the MCP spec change from late July that made the whole protocol stateless. Dropped connections now retry cleanly instead of dying.
Two — elicitation through interrupts. When a tool server needs input from a human — think "confirm this refund" — the old flow blocked a socket. The new flow raises an interrupt, your graph pauses, the human answers, the graph resumes. Same pattern LangGraph already uses for human-in-the-loop. Clean fit.
Three — MCPAdapter replaces MultiServerMCPClient. One adapter class, FastMCP-compatible, with cache=True for tool-list caching. Tool discovery used to hit every server on every run. Now the list is cached and refreshed on a schedule you control.
If you need this wired into a client project, my AI agent development service does exactly these migrations. Past builds are listed in my project log.
Benchmark: before and after on my stack
I measured 200 tool calls per setup on a ₹6K VPS in Mumbai, Valkey on the same box, Postgres for checkpoints.
| Setup | P95 tool call | Cold start (5 servers) | Reconnect failures / 200 | Notes |
|---|---|---|---|---|
| Old multi-server client, no cache | 4.2s | 11.8s | 17 | Every run re-listed tools |
| New MCPAdapter, cache off | 3.4s | 6.1s | 3 | Stateless reconnects help |
| New MCPAdapter, cache on | 2.6s | 2.2s | 1 | Tool list cached 10 min |
| New MCPAdapter, cache on plus Valkey | 2.6s | 1.4s | 0 | Shared cache across workers |
Tool-list caching is the big win. Discovery across five servers cost me 9 seconds on cold start. Cached, it costs zero. Stateless retries fixed the rest.
Short version. Same tools. Same servers. One-third the latency. Fewer midnight pages.
Migration code: Python
Old code first, so you can see the shape of the change:
# BEFORE: old multi-server client (still works, but no cache, session-bound)
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"docs": {"transport": "streamable_http", "url": "https://docs.internal/mcp"},
"crm": {"transport": "streamable_http", "url": "https://crm.internal/mcp"},
})
tools = await client.get_tools() # hits servers EVERY call
New code with the adapter, cache on, and elicitation wired through interrupts:
# requirements: langchain-mcp>=0.2, langgraph, valkey
import asyncio
from langchain.mcp import MCPAdapter
from langgraph.graph import StateGraph, START, END
SERVERS = {
"docs": {"transport": "streamable_http", "url": "https://docs.internal/mcp"},
"crm": {"transport": "streamable_http", "url": "https://crm.internal/mcp"},
"billing": {"transport": "streamable_http", "url": "https://billing.internal/mcp"},
}
adapter = MCPAdapter(SERVERS, cache=True, cache_ttl=600)
async def main():
tools = await adapter.get_tools() # cached after first fetch
print("tools loaded:", len(tools))
asyncio.run(main())
Elicitation as an interrupt — the billing server asks for human approval mid-run:
from typing import TypedDict
from langgraph.types import interrupt, Command
class BillState(TypedDict):
invoice_id: str
approved: bool
def billing_node(state: BillState):
# server requested elicitation: pause graph, ask human, resume with answer
answer = interrupt({"ask": "Refund invoice " + state["invoice_id"] + "?"})
if str(answer).lower() == "yes":
return Command(update={"approved": True}, goto="refund")
return Command(update={"approved": False}, goto=END)
TypeScript note for Next.js teams: the same pattern exists in the TS SDK via client.request with an elicitation handler callback. I keep Python as the agent runtime and call it from Next.js over HTTP. My Next.js and AI integration notes describe that split. For a price quote, contact me here.
Cache config as JSON, shared between my Python worker and the n8n watchdog:
{
"mcp_adapter": "sep-2026",
"cache": true,
"cache_ttl_seconds": 600,
"servers": ["docs", "crm", "billing", "geo", "files"],
"elicitation": "interrupt",
"ledger_log": true
}
Bash helper I run after each deploy to verify every server answers and the cache warms:
#!/usr/bin/env bash
set -euo pipefail
python3 /tmp/mcp_health.py
echo "cache keys:"
valkey-cli --raw SCAN 0 TYPE string | grep -c "mcp:tools" || true
War story 1: the stale tool list that refunded nothing
September 4, morning. I migrated the billing bot, turned cache=True, and celebrated. Then the billing server shipped a new refund_partial tool at noon. My bot kept calling the old refund_full tool for six hours. Three partial-refund requests went out as full refunds. Total damage: ₹18,400 in excess refunds that I personally covered for the client.
No error anywhere. The tool list was cached. The cache was doing its job. My TTL was the bug — 3600 seconds, one full hour, with no invalidation on deploy.
Exact log line that told the story: tool=refund_full args=OK cache_age=3420s server_version=v14 client_version=v13.
Fix: TTL down to 600 seconds, plus a deploy hook that flushes mcp:tools keys in Valkey whenever any MCP server redeploys. Cost of the fix: 20 minutes. Cost of the lesson: ₹18,400. Cache invalidation remains one of the two hard problems. The other one is naming things. Off-by-one errors are the third.
War story 2: interrupts that never resumed
September 5, night. The Rajkot catalog bot asked for human approval on a bulk price change — 400 SKUs. The interrupt fired. The manager tapped "yes" on his phone. Nothing happened. The graph sat paused for 11 hours until I found it at 06:00.
Error in the checkpoint table: status=interrupted resume_token=NULL thread_id=price-bulk-09.
Root cause: I had built the resume endpoint to look up the thread by HTTP session cookie. Stateless core means no session. The resume token arrived, matched nothing, and died silently. P95 for that approval path was infinity. Eleven hours of a price change stuck in limbo.
Fix: pass the LangGraph thread ID inside the interrupt payload itself, and resume with Command(resume=answer) keyed by that ID. Tested with five fake approvals before going live. Resume P95 now 1.1 seconds.
Rule: never key resumption on connection state. Key it on IDs inside the payload. Stateless means exactly that.
Production Trade-offs: when NOT to use this
Do not enable tool caching on servers whose tool lists change often without versioning. If your team deploys MCP servers daily with new tools, a 10-minute stale window will bite you the way it bit me. Either version your tool lists or keep TTL under 120 seconds.
Do not use interrupts for machine-answerable questions. Elicitation is for humans. If the answer can come from a database lookup, do the lookup in the node. Every interrupt adds human latency — minutes, not milliseconds — and a paused graph holds a checkpoint row in Postgres. Hundreds of paused graphs mean a fat table.
Do not migrate all bots in one night. I did four across two nights and that was already aggressive. Migrate the lowest-traffic bot first, run it 48 hours, compare ledger P95, then move the rest. The old client and the new adapter can run side by side against the same servers.
Do not skip the ledger. Log every tool call with server name, tool name, cache hit or miss, latency, and cost. My nightly report flags cache hit rates below 70 percent and any server with more than 3 reconnects per 100 calls. That report caught both war stories above within a day.
Related: my frontier model routing guide uses the same adapter with per-model cost caps.
Sources I verified against: LangChain docs at https://docs.langchain.com, MCP spec at https://spec.modelcontextprotocol.io, LangGraph interrupts at https://docs.langchain.com/langgraph.
Frequently Asked Questions
What is MCPAdapter in LangChain and why does it replace MultiServerMCPClient?
MCPAdapter is the new class in the langchain.mcp package that loads tools from one or more MCP servers with a stateless core and optional tool-list caching. It replaces MultiServerMCPClient because the protocol dropped server-side sessions, so connection-bound clients caused reconnect storms. My P95 fell from 4.2s to 2.6s after switching with cache on.
How does elicitation work with interrupts in LangGraph?
When a tool server needs human input, the node calls interrupt() with the question, the graph checkpoints and pauses, and your UI delivers the question to a person. The person answers, you resume the same thread with Command(resume=answer), and the node continues. Key resumption on thread IDs inside the payload, never on session cookies, or resumes fail silently.
Should I enable cache True for MCP tool lists?
Yes for stable servers, with a 600-second TTL and a deploy hook that flushes the cache when any MCP server redeploys. I measured cold start dropping from 11.8s to 2.2s. Skip caching or use a short TTL under 120 seconds if your tool lists change daily, or stale tools will serve old behavior like my ₹18,400 refund incident.
How do I migrate from MultiServerMCPClient without downtime?
Run both side by side. Point the lowest-traffic bot at MCPAdapter first, keep the rest on the old client, and compare P95 and error rates from your ledger for 48 hours. Then move bots one per night. Keep server URLs identical so rollback is a one-line config change.
Bottom Line
LangChain MCPAdapter with cache on cut my P95 38 percent and killed reconnect storms — but the cache TTL cost me ₹18,400 in stale refunds and a session-keyed resume left a graph paused 11 hours. Cache with a deploy hook, resume with payload IDs, migrate one bot at a time.