MCP Adapters
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against langchain-mcp-adapters 0.3.2, langchain 1.4.3, langchain-core 1.6.5 and
langgraph 1.2.12 with the mcp 1.30.0 SDK on Python 3.12, plus langchain[mcp] 1.4.3 with FastMCP 4.0.10 and mcp
2.2.0 (in a second virtual environment) for the newer langchain.mcp namespace, and langgraph-cli 0.4.32 with
langgraph-api 0.15.1 for the Agent Server section, as verified on 2026-09-29. The running example is
DocsAssistant, which answers questions over this site’s documentation with Ollama’s llama3.1:8b chat model over the
pgvector store built in Integrating with LangChain. Ollama
was not available while the snippets were checked, so every agent run below used a scripted fake chat model (a
GenericFakeChatModel subclass that replays prepared messages, including the tool calls). The MCP servers, the
adapters, the agents and the Agent Server were the real packages, running locally; no output comes from a live model.
MCP is the protocol that lets one tool implementation serve many agent frameworks. Its architecture, primitives,
transports and security rules are covered in the Model Context Protocol sub-section (start with
MCP Architecture) and are not repeated here. This page is the LangChain side of the
contract: how a create_agent agent (see Agents with create_agent)
consumes the tools, resources and prompts of an MCP server, and how a LangGraph agent becomes an MCP server itself.
The equivalent Python, Java and TypeScript clients, and the protocol-level handling of input_required results, are on
Building MCP Clients; the same LangChain4j capability is on
LangChain4j Agents & MCP.
Two packages, one job
LangChain currently has two MCP integrations, and a search for "LangChain MCP" returns both:
langchain-mcp-adapters |
langchain.mcp |
|
|---|---|---|
Install |
|
|
Entry point |
|
|
Built on |
the first-generation |
FastMCP 4 (and |
Tools |
yes |
yes |
Resources and prompts |
yes ( |
not wrapped yet; read them with the FastMCP client |
Status |
maintained; its repository points to the migration guide |
beta: importing it raises |
The LangChain documentation now teaches langchain.mcp and describes langchain-mcp-adapters as replaced. The
adapters package still installs and works, it is what most existing agents and the LangSmith deployment examples use,
and it is the only one of the two that also loads resources and prompts. This page therefore teaches
MultiServerMCPClient first and MCPAdapter in its own section, with the differences stated in a table. See the
LangChain MCP documentation and the
migration guide.
A local DocsAssistant MCP server
Every snippet below talks to the same small server, so the outputs can be compared. It has two tools, one resource, one
resource template and one prompt over a three-page stand-in corpus (the real DocsAssistant would query pgvector), and
runs over stdio by default and over Streamable HTTP with --http. It uses the mcp 1.30 SDK’s FastMCP class, the
same first-generation SDK that langchain-mcp-adapters 0.3.2 requires. The servers themselves are built in
Python MCP Servers, which also explains why the v2 SDK renamed that class to
MCPServer.
import sys
from mcp.server.fastmcp import FastMCP
CORPUS = {
"chunking": "Split documents into overlapping chunks before embedding them.",
"pgvector": "pgvector stores embeddings in a vector column and supports HNSW indexes.",
"tool-design": "Design narrow, typed tools and return actionable errors.",
}
mcp = FastMCP("docs-assistant", host="127.0.0.1", port=8765)
@mcp.tool()
def search_docs(query: str, top_k: int = 2) -> list[dict]:
"""Search the DocsAssistant corpus and return matching pages."""
words = query.lower().split()
hits = [
{"page": page, "snippet": text}
for page, text in CORPUS.items()
if any(w in page or w in text.lower() for w in words)
]
return hits[:top_k]
@mcp.tool()
def open_issue(title: str) -> str:
"""Open a GitHub issue (simulated: nothing leaves this process)."""
if not title.strip():
raise ValueError("title must not be empty")
return f"created issue: {title}"
@mcp.resource("docs://index")
def page_index() -> str:
"""The list of corpus pages."""
return "\n".join(CORPUS)
@mcp.resource("docs://pages/{page}")
def read_page(page: str) -> str:
"""Full text of one corpus page."""
return CORPUS.get(page, "not found")
@mcp.prompt()
def summarize_page(page: str) -> str:
"""Prompt template that asks for a summary of a corpus page."""
return f"Summarize the page '{page}' in two sentences for a new contributor."
if __name__ == "__main__":
mcp.run(transport="streamable-http" if "--http" in sys.argv else "stdio")
Started with python docs_server.py --http, it listens on http://127.0.0.1:8765/mcp. The open_issue tool is a
write action: it appears in the examples so the approval and scoping patterns have something to guard, and it only
formats a string. See the MCP Python SDK documentation.
MultiServerMCPClient
MultiServerMCPClient takes a dictionary that maps a server name to a connection. A connection is a plain dictionary
whose transport key selects the shape: "stdio" (with command, args and optionally env and cwd) launches the
server as a subprocess, and "streamable_http" (with url and optionally headers and timeout) reaches a remote
one. The installed connection types also include "sse" and "websocket"; the HTTP+SSE transport is deprecated in the
protocol, as explained on MCP Transports, so use Streamable HTTP for anything new.
import asyncio
import sys
from langchain_mcp_adapters.client import MultiServerMCPClient
async def main() -> None:
client = MultiServerMCPClient(
{
"docs": {"transport": "stdio", "command": sys.executable, "args": ["docs_server.py"]},
"docs_http": {"transport": "streamable_http", "url": "http://127.0.0.1:8765/mcp"},
},
tool_name_prefix=True,
)
tools = await client.get_tools()
print(sorted(t.name for t in tools))
search = next(t for t in tools if t.name == "docs_http_search_docs")
print(search.args_schema["properties"])
print(await search.ainvoke({"query": "chunking"}))
print(sorted(t.name for t in await client.get_tools(server_name="docs")))
asyncio.run(main())
Output (server log lines omitted):
['docs_http_open_issue', 'docs_http_search_docs', 'docs_open_issue', 'docs_search_docs']
{'query': {'title': 'Query', 'type': 'string'}, 'top_k': {'default': 2, 'title': 'Top K', 'type': 'integer'}}
[{'type': 'text', 'text': '{\n "page": "chunking",\n "snippet": "Split documents into overlapping chunks before embedding them."\n}', 'id': 'lc_3aea1089-d04e-4dd1-9061-7b1d00c43aad'}]
['docs_open_issue', 'docs_search_docs']
Four things to notice:
-
get_tools()returns ordinary LangChain tools (BaseTool), whose name, description andargs_schemacome from the MCP tool definition, so they work anywhere a tool from Tools works. -
tool_name_prefix=Trueprefixes each tool with its server name (docs_http_search_docs). Without it, two servers that both exposesearch_docscollide, and the model sees ambiguous names. The design rules for names are on Tool Design. -
get_tools(server_name="docs")loads the tools of one server only. -
The tool result is a list of content blocks (
type: "text"), not a bare string. Structured output from the server arrives as JSON text in the block; the agent’s model reads it as-is.
By default every tool call opens a new session and closes it afterwards, so a stdio server is launched again for each
call, and no state survives between calls. That is the safe default for stateless tools. When a server keeps state
(a database connection, a warmed-up index), hold one session open yourself and load the tools from it with
load_mcp_tools:
import asyncio
import sys
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
async def main() -> None:
client = MultiServerMCPClient(
{"docs": {"transport": "stdio", "command": sys.executable, "args": ["docs_server.py"]}}
)
async with client.session("docs") as session: # one subprocess for the whole block
tools = await load_mcp_tools(session)
search = next(t for t in tools if t.name == "search_docs")
print(await search.ainvoke({"query": "pgvector", "top_k": 1}))
print(await search.ainvoke({"query": "tool", "top_k": 1}))
asyncio.run(main())
Both calls returned their hits (pgvector, then tool-design) from the same server process. The tools loaded from a
session stop working when the block exits. See the
langchain-mcp-adapters repository and the
LangChain MCP documentation.
Protocol era and the 2026-07-28 revision
langchain-mcp-adapters 0.3.2 sits on the first-generation Python SDK, so it opens each session with the
initialize handshake and speaks protocol revision 2025-11-25 at most. It does not use the server/discover probe or
the sessionless request shape introduced by the
2026-07-28 revision; a server built on the v2 SDK or FastMCP 4
still serves it, because those servers keep answering the older handshake. The consequence that matters in practice
is elicitation. A v1 client receives a server’s question as an elicitation/create request pushed down the session,
answered by a callback registered with Callbacks(on_elicitation=…), exactly as shown on
Building MCP Clients. Without a callback the client declares no elicitation
capability and a tool that asks a question fails. On the 2026-07-28 revision the same question is an input_required
result that the client answers by calling again; see Tools, Resources, Prompts &
Elicitation and MCP Transports. langchain.mcp (below) targets that newer shape.
The adapters also require mcp below 2.0.0. Installing them next to mcp 2.x downgrades the SDK, so give an agent
that uses langchain-mcp-adapters its own virtual environment rather than sharing one with a v2 MCP server.
MCP tools in create_agent
MCP tools are LangChain tools, so the agent code does not change: pass them in the tools list. They are asynchronous
only, so run the agent with ainvoke or astream. The script below combines the MCP tools with
HumanInTheLoopMiddleware (see Human-in-the-Loop & Time
Travel) so the one write tool, open_issue, pauses for approval while search_docs runs freely. The model is the
scripted fake: first a search_docs call, then an open_issue call, then the final answer.
import asyncio
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
class ScriptedChat(GenericFakeChatModel):
def bind_tools(self, tools, **kwargs):
return self
def call(name, args, id_):
return AIMessage(content="", tool_calls=[{"name": name, "args": args, "id": id_, "type": "tool_call"}])
async def main() -> None:
client = MultiServerMCPClient(
{"docs": {"transport": "streamable_http", "url": "http://127.0.0.1:8765/mcp"}}
)
tools = await client.get_tools()
model = ScriptedChat(messages=iter([
call("search_docs", {"query": "chunking"}, "c1"),
call("open_issue", {"title": "Explain overlap in chunking"}, "c2"),
AIMessage(content="I opened an issue about chunk overlap."),
]))
agent = create_agent(
model,
tools,
middleware=[HumanInTheLoopMiddleware(interrupt_on={"open_issue": True, "search_docs": False})],
checkpointer=InMemorySaver(),
)
cfg = {"configurable": {"thread_id": "t1"}}
out = await agent.ainvoke({"messages": [{"role": "user", "content": "Is chunking documented?"}]}, cfg)
req = out["__interrupt__"][0].value["action_requests"][0]
print("paused on:", req["name"], req["args"])
out = await agent.ainvoke(Command(resume={"decisions": [{"type": "approve"}]}), cfg)
for m in out["messages"]:
print(type(m).__name__, "|", getattr(m, "name", None), "|", str(m.content)[:90].replace("\n", " "))
asyncio.run(main())
Output:
paused on: open_issue {'title': 'Explain overlap in chunking'}
HumanMessage | None | Is chunking documented?
AIMessage | None |
ToolMessage | search_docs | [{'type': 'text', 'text': '{\n "page": "chunking",\n "snippet": "Split documents into ov
AIMessage | None |
ToolMessage | open_issue | [{'type': 'text', 'text': 'created issue: Explain overlap in chunking', 'id': 'lc_e682571b
AIMessage | None | I opened an issue about chunk overlap.
The approval gate is the client’s decision, not the server’s: nothing in the MCP protocol makes open_issue ask, and
tool annotations such as destructiveHint are untrusted hints (see
MCP Security). The approval lives in the agent, next to the tool list. See
LangChain MCP and
LangChain agents.
Tool interceptors, errors and authentication
MultiServerMCPClient takes tool_interceptors, a list of async callables that wrap every MCP tool call. An
interceptor receives an MCPToolCallRequest (name, args, server_name, headers and the LangGraph runtime) and
a handler; it may change the request with request.override(…), call the handler more than once (retry), or skip
it. Interceptors compose like onion layers, the first being outermost. The one below scopes the agent to read-only
tools and adds an Authorization header read from the environment on every call:
import asyncio
import os
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from mcp.types import CallToolResult, TextContent
READ_ONLY = {"search_docs"}
async def guard(request: MCPToolCallRequest, handler):
"""Runs around every MCP tool call: scope the tools, add a per-call header."""
if request.name not in READ_ONLY:
return CallToolResult(
content=[TextContent(type="text", text=f"{request.name} is not allowed for this agent")],
isError=True,
)
token = os.environ.get("DOCS_MCP_TOKEN", "")
print(f"[{request.server_name}] {request.name}({request.args})")
return await handler(request.override(headers={"Authorization": f"Bearer {token}"}))
async def main() -> None:
client = MultiServerMCPClient(
{"docs": {"transport": "streamable_http", "url": "http://127.0.0.1:8765/mcp"}},
tool_interceptors=[guard],
)
tools = {t.name: t for t in await client.get_tools()}
print(await tools["search_docs"].ainvoke({"query": "pgvector", "top_k": 1}))
print(await tools["open_issue"].ainvoke({"title": "x"}))
asyncio.run(main())
search_docs was forwarded with the header (the server ignores it) and printed its hit; open_issue never reached the
server and returned open_issue is not allowed for this agent. Inside an agent the blocked call arrives as a
ToolMessage with status="error" and the model’s own tool-call ID, so the model can react. The token is read from
DOCS_MCP_TOKEN at call time; it is never written in the code, and the header is the place to put a token that the
server issued for this client. What a server may accept, and why it must never forward that token to another API, is
the subject of MCP Authorization. For a fixed header on every request of one server,
set headers in its streamable_http connection instead.
Errors have their own switch. By default (handle_tool_errors=True), a tool that fails on the server becomes a
ToolMessage with status="error" and the server’s message, so the agent can correct itself. With
handle_tool_errors=False the client raises instead:
plain = MultiServerMCPClient({"docs": {"transport": "streamable_http", "url": "http://127.0.0.1:8765/mcp"}})
tool = {t.name: t for t in await plain.get_tools()}["open_issue"]
message = await tool.ainvoke({"id": "c", "name": "open_issue", "args": {"title": " "}, "type": "tool_call"})
print(type(message).__name__, message.status, message.content)
strict = MultiServerMCPClient(
{"docs": {"transport": "streamable_http", "url": "http://127.0.0.1:8765/mcp"}}, handle_tool_errors=False
)
tool = {t.name: t for t in await strict.get_tools()}["open_issue"]
try:
await tool.ainvoke({"title": " "})
except Exception as e:
print("raised", type(e).__name__, e)
Output:
ToolMessage error [{'type': 'text', 'text': 'Error executing tool open_issue: title must not be empty', 'id': 'lc_d14d696d-da98-4e93-bc3c-e3d589954b8b'}]
raised _MCPToolExecutionError Error executing tool open_issue: title must not be empty
The general error-handling patterns for tools, and wrap_tool_call middleware that applies to every tool (MCP or not),
are on Tools and Middleware. The guardrails around
tool boundaries in general are in Guardrails & Security. See
LangChain MCP — Tools.
Resources and prompts
Tools are the primitive most servers offer, but MCP servers also publish resources (readable data addressed by URI)
and prompts (parameterised message templates); the difference in who controls each is explained on
Tools, Resources, Prompts & Elicitation. MultiServerMCPClient maps them onto LangChain
types: get_resources returns Blob documents from langchain-core (URI in metadata["uri"]), and get_prompt
returns a list of HumanMessage and AIMessage objects ready to put into a conversation.
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
async def main() -> None:
client = MultiServerMCPClient(
{"docs": {"transport": "streamable_http", "url": "http://127.0.0.1:8765/mcp"}}
)
blobs = await client.get_resources("docs", uris=["docs://index", "docs://pages/pgvector"])
for b in blobs:
print(b.metadata["uri"], "|", b.as_string()[:60].replace("\n", ", "))
print(len(await client.get_resources("docs")), "resources listed")
messages = await client.get_prompt("docs", "summarize_page", arguments={"page": "chunking"})
print(type(messages[0]).__name__, messages[0].content)
async with client.session("docs") as session:
listed = await session.list_prompts()
print([p.name for p in listed.prompts])
tpl = await session.list_resource_templates()
print([t.uriTemplate for t in tpl.resourceTemplates])
asyncio.run(main())
Output:
docs://index | chunking, pgvector, tool-design
docs://pages/pgvector | pgvector stores embeddings in a vector column and supports HNSW indexes.
1 resources listed
HumanMessage Summarize the page 'chunking' in two sentences for a new contributor.
['summarize_page']
['docs://pages/{page}']
Three details follow from the output. get_resources without uris lists only the server’s concrete resources
(docs://index), so a resource template such as docs://pages/{page} has to be read by naming a full URI, or
discovered through the raw session (list_resource_templates). get_prompt needs the prompt name in advance, which
the raw session’s list_prompts supplies. And resources and prompts are not tools: an agent does not see them unless
your code puts them in front of it. The usual choices are to append the Blob text to the system prompt or to a
HumanMessage (which is what makes a resource "context" rather than "action"), to start a conversation from the
prompt’s messages, or to wrap get_resources in a small @tool of your own when the model should decide what to
read. Reading arbitrary resource URIs on the model’s behalf gives it a file reader, so restrict the wrapper to the
URIs you intend to expose. See
langchain-mcp-adapters for the API.
The newer langchain.mcp namespace
langchain[mcp] 1.4 ships langchain.mcp.MCPAdapter, which collapses MultiServerMCPClient into one async context
manager built on FastMCP. The adapter infers the transport from its target: an http(s) URL string is Streamable HTTP,
a Path to a script is launched over stdio, an in-process FastMCP server connects in memory, and a dictionary in the
standard {"mcpServers": {…}} shape puts several servers behind one adapter (with automatic {server}_{tool}
prefixes). The example below uses a FastMCP 4 stdio server (fm_server.py, a two-line search_docs tool) next to the
Streamable HTTP server started earlier, and was run in a separate virtual environment because it needs mcp 2:
import asyncio
from pathlib import Path
from langchain.mcp import MCPAdapter
async def main() -> None:
config = {
"mcpServers": {
"local": {"command": "python", "args": ["fm_server.py"]},
"remote": {"url": "http://127.0.0.1:8765/mcp"},
}
}
async with MCPAdapter(config) as adapter:
tools = await adapter.list_tools()
print(sorted(t.name for t in tools))
async with MCPAdapter(Path("fm_server.py")) as adapter:
print([t.name for t in await adapter.list_tools()])
async with MCPAdapter("http://127.0.0.1:8765/mcp") as adapter:
print([t.name for t in await adapter.list_tools()])
asyncio.run(main())
Output (the FastMCP banner and the beta warning omitted). Passing these tools to create_agent with the scripted model
ran the local_search_docs call and returned the chunk text with a success status, as in the agent example above:
['local_search_docs', 'remote_open_issue', 'remote_search_docs'] ['search_docs'] ['search_docs', 'open_issue']
The first server was a FastMCP 4 (modern era) server and the second the first-generation server, and one adapter
served both; the LangChain documentation notes that a shared MCPConfig fleet negotiates a single protocol era for all
its members, so it falls back to the older era as soon as one member only speaks that one, and that a ClientGroup
keeps each server’s own era. The differences that matter when migrating:
langchain-mcp-adapters |
langchain.mcp |
|---|---|
|
|
|
inferred from the target (URL, |
|
automatic for a multi-server config |
|
LangChain |
|
fixed: |
|
elicitation is answered through a LangGraph |
|
no wrapper yet; use the FastMCP client’s |
headers and OAuth per connection |
set on a |
Sampling and roots requests from a server are not answered by langchain.mcp: a tool call that triggers one raises
NotImplementedError. The modern protocol era has no back-channel for them, which is consistent with those features
being deprecated in the 2026-07-28 revision (see Tools, Resources, Prompts & Elicitation).
Because the namespace is in beta, pin langchain and re-read the pages linked below before upgrading. See the
migration guide and
LangChain MCP — Connections.
Exposing a LangGraph agent as MCP
The reverse direction turns DocsAssistant into a tool that other hosts (an IDE, another agent, Claude Desktop) can call. There are two routes.
The Agent Server /mcp endpoint
Every LangGraph Agent Server (the runtime behind langgraph dev and LangSmith Deployment, described on
Deployment) serves an MCP endpoint at /mcp and exposes each graph in
langgraph.json as one tool. The tool name is the graph’s name, the description comes from the description key of
the graph entry, and the input schema is the graph’s input schema. The official page warns that the default
MessagesState is too general to hand to a model, and recommends a small explicit input and output schema. This
graph does that for DocsAssistant (the node stands in for a real create_agent call):
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
class InputState(TypedDict):
question: str
class OutputState(TypedDict):
answer: str
class OverallState(InputState, OutputState):
pass
def answer_node(state: InputState) -> dict:
# a real DocsAssistant would call create_agent(...) here
return {"answer": f"DocsAssistant would answer: {state['question']}"}
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
builder.add_node("answer_node", answer_node)
builder.add_edge(START, "answer_node")
builder.add_edge("answer_node", END)
graph = builder.compile()
{
"dependencies": ["."],
"graphs": {
"docs_assistant": {
"path": "./docs_graph.py:graph",
"description": "Answers questions about this documentation site."
}
}
}
After langgraph dev --no-browser (which serves on port 2024), the same adapters client that read docs_server.py
consumed the agent as a tool:
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
async def main() -> None:
client = MultiServerMCPClient(
{"agent_server": {"transport": "streamable_http", "url": "http://127.0.0.1:2024/mcp"}}
)
tools = await client.get_tools()
for t in tools:
print(t.name, "|", t.description, "|", list(t.args_schema["properties"]))
print(await tools[0].ainvoke({"question": "How do I chunk documents?"}))
asyncio.run(main())
Output:
docs_assistant | Answers questions about this documentation site. | ['question']
[{'type': 'text', 'text': "{'answer': 'DocsAssistant would answer: How do I chunk documents?'}", 'id': 'lc_15736db2-2e9f-4fe7-bf07-777faed79fa8'}]
According to the official page the endpoint is stateless (each /mcp request is independent, so there are no MCP
sessions) and uses the same authentication as the rest of the Agent Server API; you turn it off with
"http": {"disable_mcp": true} in langgraph.json. It needs langgraph-api 0.2.3 or later. For a deployed server the
Authorization or API-key header goes in the client’s headers, from a secret store, as in the interceptor above.
This is the "agent as a tool" building block of the multi-agent patterns in
Multi-Agent & Deep Agents, with a network boundary in between.
See MCP endpoint in Agent Server.
Wrapping a graph in your own MCP server
Without an Agent Server, a compiled graph or create_agent agent can be a tool of an ordinary MCP server, which is
the right route when the agent should run over stdio inside a host such as an IDE. Any server SDK works; this one uses
the same FastMCP class as before:
from mcp.server.fastmcp import FastMCP
from docs_graph import graph # any compiled graph or create_agent(...) agent
mcp = FastMCP("docs-assistant-agent")
@mcp.tool()
async def ask_docs_assistant(question: str) -> str:
"""Ask DocsAssistant a question about this documentation site."""
result = await graph.ainvoke({"question": question})
return result["answer"]
if __name__ == "__main__":
mcp.run(transport="stdio")
Run through MultiServerMCPClient with a stdio connection to graph_as_mcp.py, it printed
ask_docs_assistant → [{'type': 'text', 'text': 'DocsAssistant would answer: What is HNSW?', …}]. Design the tool
like any other (Tool Design): one typed question in, one string out, a docstring the
calling model can decide from. A remote deployment of such a server needs the authorization rules of
MCP Authorization and the hardening of MCP Security; how to
host it is covered in Deploying Remote MCP Servers. See
LangChain MCP and the
Python SDK documentation.
Choosing and operating
-
New project, tools only:
langchain.mcpMCPAdapter, accepting its beta status. -
Resources or prompts needed, or an existing adapters-based agent:
MultiServerMCPClient, in its own virtual environment, and plan the migration. -
Java: the LangChain4j MCP client on LangChain4j Agents & MCP.
-
Trust: connect only to servers whose owners you trust, keep write tools behind approval, scope tools with an interceptor or a filtered tool list, and keep tokens in environment variables or a secret store, never in the code or the
stdioargs. Tool descriptions from a server enter the model’s context, so a hostile server can attempt prompt injection; see MCP Security and Agent Guardrails & Safety. -
Testing: the in-memory and stdio transports make MCP-backed agents testable without a network; see Testing and Testing & Debugging MCP Servers.