LangChain is introducing an alpha preview of langchain.mcp, a first-party adapter that converts any MCP server into LangChain tools ready to be passed directly to create_agent.
The connection layer is powered by FastMCP, which means all of its client capabilities are available without being re-created behind a more limited interface.
pip install "langchain[mcp]==1.4.0a2"
Connect
With MCPAdapter, you can pass any target that fastmcp.Client supports. Transport is inferred automatically, giving you a single entry point instead of a different adapter for each protocol.
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter
async with MCPAdapter("https://example.com/mcp") as adapter:
agent = create_agent("anthropic:claude-sonnet-5", await adapter.get_tools())
result = await agent.ainvoke({"messages": [{"role": "user", "content": "..."}]})
Accepted targets include a URL, a local script path (run over stdio), an in-process FastMCP server, a multi-server configuration object, or a fastmcp.Client instance you’ve already customized.
Tools returned by get_tools() retain a reference to the adapter’s client, so they remain usable after the async with block ends. The context manager governs discovery only, not the lifetime of the tools themselves.
Auth, caching, timeouts — build the client
MCPAdapter accepts two arguments: the target and elicitation. All other FastMCP options are set on a fastmcp.Client that you construct and pass in as the target. This is the approach to use when you need more than a simple connection.
from fastmcp.client import Client
from langchain.mcp import MCPAdapter
client = Client(
"https://example.com/mcp",
auth="oauth", # or a bearer token string, or any httpx auth
cache=True, # opt-in response caching
timeout=30,
)
async with MCPAdapter(client) as adapter:
tools = await adapter.get_tools()
For authentication, you can pass "oauth" to trigger the OAuth flow, a token string for bearer authentication, or an httpx.Auth instance for custom setups — see FastMCP’s auth documentation. In a multi-server configuration, you can also set headers and authentication per server (as shown below).
Caching is opt-in and disabled by default. Setting cache=True turns it on with sensible defaults and respects the server’s ttlMs and cacheScope hints; you can fine-tune it with a CacheConfig. The cache is per-client and stored in memory.
Every other option on fastmcp.Client — timeout, log_handler, progress_handler, message_handler, roots, sampling_handler — behaves exactly as documented. The adapter passes your client through without modification, so none of FastMCP’s functionality is re-implemented or limited.
A word of caution: when using elicitation="interrupt", the adapter clones your client to avoid overwriting any callback you’ve defined. Configuration such as auth, cache settings, and handlers is carried over to the clone, but cached entries are not, because the clone has its own separate store.
You can access the underlying client via adapter.client to work with prompts, resources, and any other features the adapter doesn’t directly wrap.
Multiple servers
Point the adapter at a configuration object, and it fans out to each server through a single connection, presenting your agent with one combined list of tools.
config = {
"mcpServers": {
"weather": {"url": "https://weather.example.com/mcp"},
"calendar": {
"url": "https://calendar.example.com/mcp",
"headers": {"Authorization": "Bearer ..."},
},
}
}
async with MCPAdapter(config) as adapter:
agent = create_agent("anthropic:claude-sonnet-5", await adapter.get_tools())
When multiple servers are involved, tool names are namespaced by server name — such as weather_get_forecast or calendar_create_event — eliminating any chance of collisions. With a single server, the adapter connects directly and leaves names unprefixed. Each server entry can specify its own headers, auth, transport, and timeout, so servers with different credentials can be combined in one agent. For a local server, you provide command and args instead of a url, and it’s launched over stdio. The configuration follows FastMCP’s MCP JSON schema, so any config you already use elsewhere will work here without changes.
Old and new protocol servers, side by side
MCP has transitioned from the initialize handshake to server/discover, and servers in production can be on either side of that change. FastMCP negotiates the protocol era on a per-connection basis, so the adapter works with both types without you having to choose:
# handshake-era server over SSE
legacy = MCPAdapter("https://legacy.example.com/sse")
# modern-era server over streamable HTTP
modern = MCPAdapter("https://modern.example.com/mcp")
Separate adapters negotiate independently and can run at the same time, each using its own protocol era. This scenario is covered by integration tests that spin up one server from each era and call both.
There’s one key rule to remember: a multi-server configuration exposes a single protocol era, so the oldest backend determines the era for the entire group. If you mix a handshake-era server into the config, the modern backends are pulled back to the handshake era — they still function, but any features gated to the newer era are lost. Keep a legacy server in its own adapter if you want the rest to stay on the modern protocol:
async with (
MCPAdapter({"mcpServers": {...modern servers...}}) as modern,
MCPAdapter("https://legacy.example.com/sse") as legacy,
):
tools = await modern.get_tools() + await legacy.get_tools()
Results
All tools are asynchronous. If an MCP tool runs but reports an error, the result is a ToolMessage with status="error" that includes the server’s own error text, allowing the agent to recognize the problem and try again. Transport failures and content that cannot be converted raise exceptions instead, since the model can’t act on them.
Structured output rides along on the tool message artifact:
from langchain.mcp import MCPToolArtifact
artifact: MCPToolArtifact | None = tool_message.artifact # None when there is no structured content
artifact["structured_content"]
Elicitation — servers that ask questions mid-call
Some MCP tools require additional input before they can complete. With opt-in enabled, the request appears as a LangGraph interrupt(), so the human who is already reviewing the agent’s work can answer the server’s question at the same time.
adapter = MCPAdapter(target, elicitation="interrupt")
This feature is opt-in rather than enabled by default because declaring it on the wire is a commitment: an agent with no human in the loop cannot honor it. If left unset, no elicitation capability is advertised, and a server whose tool requires an answer will decline the call instead of proceeding without one.
The run stops with a typed payload, and resumes with one answer per request key:
from langgraph.types import Command
result = await agent.ainvoke({"messages": [...]}, config)
[pause] = result["__interrupt__"]
pause.value["type"] # "mcp_elicitation"
pause.value["tool_name"] # the tool that is waiting
pause.value["requests"] # each question, in the order to ask them
answer = {"responses": {key: {"action": "accept", "content": {"guests": 4}}}}
result = await agent.ainvoke(Command(resume=answer), config)
Each request is refined by its mode: a "form" request includes a requested_schema that the answer must satisfy, while a "url" request provides an address for the human to visit. Answers are similarly narrowed by action: "accept" (with content), "decline" (skip the question and let the call continue), or "cancel" (abandon the tool call entirely). As with any interrupt, this requires a checkpointer.
Sampling and roots are not handled through interrupts; you should leave those to your client’s own handlers.
The handler types — MCPElicitationInterrupt, MCPElicitationRequest, MCPElicitationResponse, MCPElicitationResume, and the ELICITATION_INTERRUPT_TYPE discriminator — are located in langchain.mcp.elicitation.
Further reading
- FastMCP client docs — auth, caching, transports, and handler configuration
- Client transports — what each target type infers
- Elicitation — the underlying protocol feature
Please note this is an alpha release: the interface may change before the final 1.4.0. We’re actively seeking feedback on the API shape.



