If your agents reach their tools over MCP, the package you wired them with has just been replaced. Since LangChain 1.4.0, which reached PyPI on September 3, 2026, MCP support ships in the core langchain.mcp namespace, built on FastMCP, and it replaces the standalone langchain-mcp-adapters package. The new MCPAdapter collapses the old client into one class, turns a server’s mid-call questions into LangGraph interrupts, and quietly drops several features you may be standing on. This post walks the migration as the docs specify it, what the new adapter does that the old client could not, and where the beta label should slow you down.
One class replaces the client, and the config drops its transport key
The old package’s center was MultiServerMCPClient: a config dict in, a flat list of tools out. LangChain’s migration guide puts the change in one line: the old client “is collapsed into a single MCPAdapter class”. The old pattern looked like this:
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"math": {
"transport": "stdio",
"command": "python",
"args": ["/path/to/math_server.py"],
},
"weather": {
"transport": "http",
"url": "http://localhost:8000/mcp",
},
})
tools = await client.get_tools()
The replacement is an async context manager, and get_tools() becomes list_tools():
from langchain.mcp import MCPAdapter
config = {
"mcpServers": {
"math": {
"command": "python",
"args": ["/path/to/math_server.py"],
},
"weather": {"url": "http://localhost:8000/mcp"},
}
}
async with MCPAdapter(config) as adapter:
tools = await adapter.list_tools()
Two details in that diff matter more than the renames. The per-server transport key is gone because the adapter infers the transport, and the config is now the standard MCPConfig shape, the same mcpServers dict every other MCP client reads. Your agent’s server list stops being a LangChain dialect. Install follows the same consolidation: you uninstall langchain-mcp-adapters and add the extra with pip install "langchain[mcp]", which pins FastMCP 4 underneath.
The adapter infers the transport from the target you hand it
MCPAdapter takes one target argument and works out how to reach it. Per the MCP page in the LangChain docs, an http or https URL is reached over streamable HTTP, a Path is launched as a subprocess over stdio, an in-process FastMCP server is connected in memory with no socket, an MCPConfig dict fans out to several servers, and a prebuilt fastmcp.Client passes through for full control. The quickstart is three lines of tool discovery:
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter
async with MCPAdapter("https://example.com/mcp") as adapter:
tools = await adapter.list_tools()
agent = create_agent("claude-sonnet-5", tools)
Each returned tool keeps a reference to the client, so the agent it powers keeps working after the context closes. One guard is worth knowing before it surprises you: FastMCP resolves a bare string by testing it as a filesystem path before testing it as a URL, so a string naming an existing .py file would launch that file as a subprocess. Because strings are exactly the form a target arrives in from configuration, or from a model, the adapter “rejects strings that don’t match the shape of a URL”. If you want stdio, you say Path, which is the right default for a value an agent might have influenced.
Elicitation now pauses the run as a LangGraph interrupt
Elicitation is the piece of MCP most builders have not used yet. The MCP specification defines it as “a standardized way for servers to request additional information from users through the client during interactions”: a booking server can ask for a date in the middle of a tool call instead of failing it. The spec also flags the feature as newly introduced, with a design that may evolve.
The old package answered these requests through a callback you registered on the client. The migration guide is explicit about the new shape: “Elicitation moved from a callback registered on the client to a LangGraph interrupt, and it is now on by default.” When a server asks, the run pauses exactly the way a human-in-the-loop approval pauses it, a person answers, and you resume with Command(resume={"responses": {key: answer}}), where each answer accepts, declines, or cancels. It is the same LangGraph machinery we dissected in LangGraph human-in-the-loop: interrupt vs middleware, now driven by a third-party server rather than by your own policy.
The trap: elicitation is armed on every client the adapter builds, with no opt-in, and resuming needs persistence. Wire a checkpointer before the first server question arrives, or the paused run has nowhere to wait.
That default cuts both ways. A server you connect today can start asking questions tomorrow without any change on your side, which is delightful in a chat app and unwelcome in an unattended pipeline. The tools page documents the payload types in langchain.mcp.elicitation, and a prebuilt client carrying its own elicitation handler is honored instead of overridden, which is your escape hatch for headless runs.
Prompts, resources, sampling and roots did not make the move
The new namespace covers tools, and for now only tools. The migration guide marks get_prompt and get_resources as not supported: MCP prompts and resources have no langchain.mcp wrapper yet, and the guide’s suggested interim is reading them straight off the FastMCP client. Server-initiated sampling and roots requests fare worse: a tool call that triggers either raises NotImplementedError. The reason is protocol-level, not a LangChain choice. Current MCP runs without a persistent session, in the sense our stateless MCP glossary entry unpacks, and without one there is no live back-channel for a server to call into mid-request. The migration guide notes that FastMCP 4 removed ctx.sample() and ctx.list_roots() entirely for the same reason.
Transports thinned out too. Per the same guide, SSE still works through an explicit SSETransport, but the protocol deprecated it in favor of streamable HTTP, and WebSocket has no FastMCP transport at all. And the handle_tool_errors flag is gone with the behavior now fixed: a tool result carrying isError=True reaches the model as a ToolMessage with status="error", so the agent can read the server’s message and correct itself, while transport failures raise, because a model cannot act on a dropped connection. That is the right split, but if your code branched on the old flag, it is your diff to write.
Auth moved onto the FastMCP client, and approval stays your job
Authentication is where the FastMCP foundation pays off. Per the auth docs, the client’s auth argument takes a static bearer token, any httpx.Auth, or the literal string "oauth", which runs the full OAuth 2.1 flow: discovery, dynamic client registration, the browser redirect, and the token exchange. Per-server and per-user credentials are both supported, so a deployed agent can reach a server as the caller rather than as one shared service account, which is half of what agent authorization asks for.
What the platform does not do is decide which tools deserve to run. An MCP server can flag a tool with a destructiveHint annotation, and the adapter surfaces it under metadata["mcp"]["tool"]["annotations"]["destructive_hint"]:
def is_destructive(tool) -> bool:
annotations = (
(tool.metadata or {})
.get("mcp", {})
.get("tool", {})
.get("annotations", {})
)
return annotations.get("destructive_hint", False)
Feed that into HumanInTheLoopMiddleware with a when predicate and you gate whatever destructive tools a server exposes without hardcoding names, a cleaner tool approval setup than the old interceptors, which are themselves replaced by @wrap_tool_call middleware. Remember the hint is the server’s self-description: a hostile or compromised server can lie about it, which is the tool poisoning problem, and elicitation adds a channel for a server to phish your operators. The spec’s own security best practices page covers the confused deputy and token passthrough patterns, and the elicitation spec is blunt that servers “MUST NOT use elicitation to request sensitive information”. A rule a server must follow is a rule an attacker will not, so treat every server question the way you treat model output touched by prompt injection: reviewed, never auto-answered.
Beta is a real label, so time the migration deliberately
The namespace is beta and the docs say so plainly: importing from langchain.mcp raises a LangChainBetaWarning, and the API may change. The cadence backs the label. After 1.4.0 on September 3, PyPI’s release data shows three patch releases in 25 days: 1.4.1 on September 16, 1.4.2 on September 18, and 1.4.3 on September 28. Meanwhile the changelog states the new namespace “replaces the standalone langchain-mcp-adapters package”, yet the old package itself sits at 0.3.2 from August 6, 2026, not yanked and carrying no deprecation notice in its PyPI metadata. Nothing breaks this week if you stay put.
So the decision hangs on which MCP features your code touches, not on urgency. If your MCP usage is tool calling, migrate now: the renames are mechanical, the config gets more standard, and the guide even ships a copy-paste migration prompt for a coding agent. If you stand on prompts, resources, sampling, SSE or WebSocket, the old package remains the only thing that runs your code, so stay on it, pin it, and watch the gaps: new capability is landing in core, not in the package the docs call replaced.
The takeaway
LangChain 1.4 makes MCP a first-class part of the framework: one MCPAdapter for every transport, elicitation answered through the interrupt machinery you already use for approvals, and real OAuth underneath. The migration is small where you use MCP for tools and impossible where you use what did not move, so read your imports before the guide. And treat the adapter layer the way we argued in AI framework vs custom stack: it is a layer you should keep the right to replace, which this migration, transport by transport, just proved.