Latest Results
docs(langchain): runnable `langchain.mcp` examples (#39976)
Stacked on #39939 — review that first; this branch adds only
`libs/langchain_v1/examples/mcp/`.
Ten self-contained scripts, one idea each. Every one starts whatever MCP
server it needs, so `uv run examples/mcp/<name>.py` is the whole
workflow.
| Example | Shows | Model | Network |
|---|---|:-:|:-:|
| `transports.py` | one adapter over in-memory, stdio, and HTTP | | |
| `remote_server.py` | pointing the adapter at a public MCP server | ✅ |
✅ |
| `multi_server.py` | several servers behind one adapter, tools prefixed
per server | ✅ | |
| `graph_factory.py` | one per-user MCP fleet behind a `langgraph dev`
graph factory | ✅ | |
| `protocol_eras.py` | one agent holding tools from both MCP protocol
eras | ✅ | |
| `tool_errors.py` | a failing tool reaching the model so it can retry |
✅ | |
| `elicitation.py` | a server asking a human mid-call, via `interrupt()`
| ✅ | |
| `destructive_interrupt.py` | gating destructive tools behind approval,
from tool metadata | ✅ | |
| `auth_bearer.py` | a server behind a static bearer token | | |
| `auth_oauth.py` | a full OAuth 2.1 flow with dynamic client
registration | | |
Each was run against a real model (or a real `langgraph dev` server)
before committing, and its output is what the docstring claims.
A few choices worth knowing about:
- **Servers come from FastMCP's own `run_server_in_process`**, not
hand-rolled uvicorn plumbing, so the examples teach the adapter rather
than how to start a server.
- **HTTP appears only where it is the subject.** `multi_server.py` names
its backends over stdio, which is less machinery and a better
illustration, since a fleet addresses each backend independently.
- **`remote_server.py` hits DeepWiki**, a public MCP server, where the
URL is the entire configuration. It prints the tool call so the answer
is visibly the remote server's work rather than the model's memory.
- **`tool_errors.py` pins the model with a system prompt.** Without it
the model answers the arithmetic from memory and the error path never
runs.
- **`destructive_interrupt.py` derives the approval gate from
metadata**, reading each tool's
`metadata["mcp"]["tool"]["annotations"]["destructive_hint"]` to build
the `HumanInTheLoopMiddleware` `interrupt_on` map — so any tool a server
flags as destructive pauses for approval, no tool names hardcoded.
- **`auth_oauth.py` opens a browser tab.** The demo authorization server
auto-approves, so it redirects straight back — but it is the one example
that cannot run unattended.
`graph_factory.py` is registered by a `langgraph.json` and run under
`langgraph dev` rather than invoked directly. It shows a per-user MCP
fleet: one shared `httpx` connection pool for everyone, a per-user
`ClientGroup` built each run, and per-user discovery caching keyed on
the caller's identity read off the injected `ServerRuntime`.
`run_graph_factory_demo.py` is an end-to-end driver that stands up two
guarded MCP servers plus a `langgraph dev` server with custom auth and
runs the graph once per user; `auth.py`, `langgraph.json`, and
`_fleet_servers.py` support it. `_servers.py` holds the small MCP
servers the examples share, `_stdio_server.py` is the entry point
launched as a subprocess over stdio, and neither `_`-prefixed helper is
part of the API being demonstrated.
`examples/*` picks up the same two ruff exemptions `scripts/*` already
has, for printing and for not being a package.
## Release note
No library changes — examples only.
---
*Prepared with the assistance of an AI agent.*
---------
Co-authored-by: Hunter Lovell <hunter@hntrl.io>
Co-authored-by: Hunter Lovell <40191806+hntrl@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydney@langchain.dev> feat(langchain): `langchain.mcp` namespace, `MCPAdapter` (#39939)
Adds `langchain.mcp`: adapt an MCP server into LangChain tools ready for
`create_agent`.
```python
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter
adapter = MCPAdapter("https://example.com/mcp")
agent = create_agent("anthropic:claude-sonnet-5", await adapter.list_tools())
```
`langchain-mcp-adapters` stays the place for the full surface
(interceptors, callbacks, prompts, resources). This is the short path
for the common case.
## Public API
`MCPAdapter(target, *, elicitation=None)`, with a `client` property,
`list_tools(*, cache_mode="use")`, and async context management.
`as_langchain_tool(tool, client, *, elicitation=None)` converts a single
tool for callers managing their own client.
`MCPAdapter` accepts anything `fastmcp.Client` accepts — a URL, `Path`,
in-process server, `ClientTransport`, `MCPConfig` (or its dict form), or
a pre-built `Client` — plus a FastMCP `ClientGroup` for a fleet of
servers behind one client. Inference is FastMCP's, so new target types
work without changes here. The target union is `MCPAdapterTarget`; it is
not part of the package's public surface (it is only useful for
annotating a `target`), but it stays importable from
`langchain.mcp.adapter` for that purpose. An `MCPConfig` naming several
servers yields one prefixed toolset from one adapter:
```python
MCPAdapter({"mcpServers": {"notes": {"command": "python", "args": ["notes.py"]},
"web": {"url": "https://example.com/mcp"}}})
# -> ["notes_read_note", "web_get_weather", ...]
```
## Tool conversion
Tool results are ported from `langchain-mcp-adapters`, so a call reaches
a model in the same shape either way. Results become LangChain content
blocks (audio raises `NotImplementedError`); structured content becomes
an `MCPToolArtifact`; `args_schema` is the tool's `input_schema`.
Tool *metadata* is grouped under a single `mcp` namespace so a consumer
can tell an MCP tool's provenance apart and keep tool-level fields
distinct from the serving server's identity:
```python
tool.metadata == {
"mcp": {
"tool": {
"annotations": {"destructive_hint": True, ...}, # snake_case, from tool.annotations
"_meta": {...}, # verbatim MCP `_meta`
},
"server": {"name": "files", "version": "2.1.0", ...}, # from the live client connection
},
}
```
Server identity comes off the connection (a `Tool` carries no server
field) and is read at conversion time while the client is connected.
This metadata rides onto the LangChain tool's `metadata`, so it also
lands on the tool's traced run — `mcp.server.name` becomes filterable in
traces. The examples PR uses the destructive hint to gate a tool behind
human approval.
An `isError=True` result becomes a `ToolMessage` with `status="error"`
carrying the server's own error content, so the agent can correct itself
instead of the run ending. Transport failures and unconvertible content
still raise.
FastMCP clients are reentrant and reference-counted, so the adapter adds
no second layer — tools hold the client and stay callable after the
adapter's context exits.
## Discovery caching
`list_tools(cache_mode=...)` selects how discovery interacts with the
client-side response cache (SEP-2549): `use` (default) serves a cached
tool list within the server's TTL hint, `refresh` calls the server and
repopulates it, `bypass` skips the cache. The cache and its
per-principal isolation are configured on the client itself
(`Client(cache=...)`); this only selects how discovery reads it. The
default is `use` so a configured cache is honored — note this differs
from a bare `ClientGroup.list_tools()`, whose own default is `refresh`.
## Elicitation
Some MCP tools need input before they can finish.
`elicitation="interrupt"` surfaces the question as a LangGraph
interrupt:
```python
adapter = MCPAdapter(server, elicitation="interrupt")
paused = await agent.ainvoke({"messages": [...]}, config)
[question] = paused["__interrupt__"][0].value["requests"]
# {'key': 'date', 'message': 'What date would you like to book?', 'mode': 'form',
# 'requested_schema': {...}}
await agent.ainvoke(
Command(resume={"responses": {question["key"]: {"action": "accept", "content": {"date": "2026-08-26"}}}}),
config,
)
```
Answers correlate by the server's own request keys, so nothing extra is
needed on resume. Several requests in one round share one interrupt;
successive rounds each get their own. The payload types live in
`langchain.mcp.elicitation`, split so the type system carries the
protocol's rules: `mode` discriminates form from URL requests, and only
an accept can carry content, narrowed to the scalar shapes the wire
accepts.
It is opt-in because a declared capability is a promise on the wire —
servers only build flows that depend on it once a client says yes — so
left unset, a server whose tool *requires* an answer refuses the call
rather than running without one. The loop is driven through
`session.call_tool(..., allow_input_required=True)` rather than a
FastMCP handler, which would convert the `GraphInterrupt` into an MCP
error. Resuming re-issues the call from its first round, since the
interrupt unwound it; a server that asks before doing work repeats
nothing, which the tests assert by counting tool-body executions.
Scope is elicitation only. Embedded sampling and roots requests raise,
since driving the loop by hand bypasses the FastMCP callbacks that
answer them, as does a continuation round carrying only `request_state`
— that is the protocol's long-running-work channel, and serving it would
mean polling a remote server from inside a tool call.
## Building on FastMCP
Handing the client to FastMCP is what keeps this small. Most of the
protocol surface is inherited rather than written:
- **Negotiation, per server.** FastMCP speaks both the 2025-11-25
`initialize` handshake and the 2026-07-28 `server/discover` revision,
and picks per connection — so one agent can hold tools from servers on
different revisions at once, with no protocol mode to choose. There is a
test that builds exactly that agent.
- **Transports and auth.** Inference covers URLs, script paths,
in-process servers, and config dicts, while `auth="oauth"`, bearer
tokens, custom `httpx` auth, and TLS via `verify` all arrive without
code here.
- **Fleets.** A `ClientGroup` target lets one adapter serve a fleet of
servers, prefixing each server's tools and routing every call back to
the client that serves it.
- **Connection lifecycle.** Clients are reentrant and reference-counted,
so tools outlive the adapter's context without a second layer of
bookkeeping, and requests race the background session task so a dead
HTTP session raises instead of hanging a tool call.
- **The input-required flow (SEP-2322).** Elicitation intercepts a round
at the session layer instead of reimplementing the multi-round protocol.
- **In-process servers.** The whole unit suite drives real MCP servers
with no subprocess and no socket.
This requires `fastmcp>=4.0.0` — the GA line whose multi-server config
routes to modern-protocol servers, which the `MCPConfig` and
`ClientGroup` support rely on.
## Release note
New `langchain.mcp` namespace. `MCPAdapter` adapts any target
`fastmcp.Client` accepts — a URL, a local script, an in-process server,
an `MCPConfig` naming several servers, or a pre-built client — as well
as a FastMCP `ClientGroup`, into LangChain tools ready for
`create_agent`. `MCPAdapter.list_tools(cache_mode=...)` discovers tools
with optional client-side response caching. `as_langchain_tool` converts
a single MCP tool for callers managing their own client. Tool metadata
(annotations, `_meta`, and the serving server's identity) is grouped
under an `mcp` namespace on each tool. `elicitation="interrupt"`
surfaces a server's mid-call questions as LangGraph interrupts. Requires
the `mcp` extra: `pip install "langchain[mcp]"`.
## Notes for review
- A `Path` or script-path target launches a local subprocess with no
opt-in keyword. Intended, and the target comes from application code
rather than a model, but it loosens the previous default and is awkward
to walk back once released.
- Tool metadata is traced. Whatever a server puts in `_meta` reaches the
tool's LangSmith run verbatim; keeping it nested under `mcp.tool._meta`
(rather than flattened) keeps that clearly demarcated.
- Two FastMCP internals are load-bearing, and both now fail loudly
rather than silently. `elicitation="interrupt"` installs a sentinel
handler purely to trip the SDK's identity comparison against its default
callback, which is the only way the SDK lets a client declare the
capability; a test asserts the negotiated capability so a change
upstream cannot quietly stop servers asking. `_await_monitored` reaches
for FastMCP's private `_await_with_session_monitoring`, without which a
session dying mid-elicitation hangs the tool call; the fallback now
warns.
- `tests/unit_tests/mcp/conftest.py` grants `blockbuster` two narrow
allowances, both caused upstream in FastMCP: entering a server `Context`
resolves an optional dependency through `importlib.metadata` on every
in-process request, and `mcp.client.session` imports `jsonschema` lazily
on first tool call.
---
*Prepared with the assistance of an AI agent.*
---------
Signed-off-by: Hunter Lovell <40191806+hntrl@users.noreply.github.com>
Co-authored-by: Hunter Lovell <hunter@hntrl.io>
Co-authored-by: Hunter Lovell <40191806+hntrl@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydney@langchain.dev> Latest Branches
0%
visnu64:fix/reasoning-model-structured-output 0%
sydney-runkle/langchain/mcp-examples 0%
sydney-runkle/langchain/simplify-mcp-adapter © 2026 CodSpeed Technology