LangGraph Deep Agents Tools: MCP Servers and Python
A LangGraph deep agent is only as useful as the tools it can call, and only as sharp as the tool list it has to choose from. Every tool you add is a name, a description and a JSON schema that the model reads on every single turn. That makes the tool surface a context budget, not a feature list. This part of the series covers the three ways to give a deep agent tools (plain Python functions, LangChain tools, and MCP servers), the built-in harness tools you get for free, and how to hide the ones you do not want with harness profiles so the model has fewer, better choices.
LangGraph Deep Agents, a 10-part series. ← Part 2 · Part 4 →
Everything below follows the public Deep Agents tools docs, the harness profiles docs and the LangChain MCP docs.
How do you add custom Python tools to a LangGraph deep agent?
You pass them to create_deep_agent through tools=. That is the whole API. The Deep Agents tools docs say it accepts "any callable, such as plain functions, LangChain @tool-decorated functions, or tool dicts", and it infers the schema from the function signature and docstring, so for most tools you never write a schema by hand.
The docs' own example is a web search function:
from typing import Literal
from tavily import TavilyClient
from deepagents import create_deep_agent
tavily_client = TavilyClient()
def internet_search(
query: str,
max_results: int = 5,
topic: Literal["general", "news", "finance"] = "general",
include_raw_content: bool = False,
):
"""Run a web search"""
return tavily_client.search(
query,
max_results=max_results,
include_raw_content=include_raw_content,
topic=topic,
)
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
tools=[internet_search],
)
Two details in that snippet do the real work. The type hints become the argument schema, and a Literal annotation becomes an enum the model can see, which is far better than a free-text topic: str that invites guesses. The docstring becomes the description. "Run a web search" is terse, and that is a feature: it is four words the model pays for on every turn, not a paragraph.
Because the input is just a callable, anything in the LangChain tools ecosystem drops in unchanged. If you already have a @tool-decorated function from a plain LangChain agent, the deep agent takes it as is.
What built-in tools does a LangGraph deep agent already have?
Before adding anything, know what is already in the list. Per the built-in harness tools table, every deep agent ships with:
ls,read_file,write_file,edit_file,glob,grepfor the virtual filesystemdelete(requiresdeepagents>=0.7)executefor shell commands, on sandbox backends onlytaskfor spawning a subagent
Planning with write_todos is opt-in through TodoListMiddleware, not on by default.
So a "small" agent with three custom tools is really carrying a dozen. That is the baseline your own tools compete against for the model's attention, and it is why the next two sections matter more than they look.
How do you connect MCP servers to LangGraph deep agents?
Model Context Protocol servers expose tools over a standard protocol. LangChain now ships the bridge in the core package: install the extra with pip install "langchain[mcp]", then use MCPAdapter from langchain.mcp.
A note if you have older code: earlier guides used the separate langchain-mcp-adapters package. The MCP docs say the langchain.mcp namespace requires langchain[mcp]>=1.4.0, is in beta (it emits a LangChainBetaWarning once per process), and link a migration guide from langchain-mcp-adapters. New code should use MCPAdapter.
The deep agents docs show the pattern: open the adapter, list its tools, and hand them to create_deep_agent:
import asyncio
from deepagents import create_deep_agent
from langchain.mcp import MCPAdapter
async def main():
config = {"mcpServers": {"my_server": {"url": "http://localhost:8000/mcp"}}}
async with MCPAdapter(config) as adapter:
tools = await adapter.list_tools()
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
tools=tools,
)
await agent.ainvoke(
{
"messages": [
{"role": "user", "content": "Use the MCP server to help me."}
]
},
config={"configurable": {"thread_id": "1"}},
)
The thread_id in the run config names the conversation thread the run is checkpointed under.
MCPAdapter infers the transport from what you pass it (Transports):
- an
http/httpsURL string is reached over Streamable HTTP - a
Pathto a script is launched over stdio, one subprocess per adapter - an in-process
FastMCPserver connects in memory, which the docs call ideal for tests - an
MCPConfigdict withmcpServersputs several servers behind one adapter, as above
One sharp edge the docs call out: a str target must be a URL. FastMCP tests strings as file paths before URLs, so a string naming an existing .py file would launch it as a subprocess. MCPAdapter rejects non-URL strings for exactly that reason. Pass a Path when you mean a script.
MCP is where tool budgets blow up. A single server can expose dozens of tools, and list_tools() gives you all of them. Nothing stops you from filtering that list by name before passing it to tools=, and you usually should: the model does not need a server's admin tools to answer a support question.
How do harness profiles hide tools in LangGraph deep agents?
Filtering your own list handles your tools. For the harness's tools, use a harness profile. A HarnessProfile is registered against a provider or a provider:model key, and create_deep_agent applies it whenever a matching model is selected, with no change to your agent code.
The field that matters here is excluded_tools. The profiles docs describe it as a post-injection filter matched by tool name, "so it can drop both user-supplied tools and tools added by harness middleware". From the profiles docs:
from deepagents import HarnessProfile, register_harness_profile
register_harness_profile(
"my_provider",
HarnessProfile(
excluded_tools=frozenset({"execute"}),
system_prompt_suffix="Respond in under 500 words.",
),
)
Three rules worth knowing:
- Profiles merge. A provider-level profile and a model-level profile combine; unset model fields inherit, explicit ones override. Re-registering under an existing key merges on top of the old profile rather than replacing it.
- Hide tools, do not rip out scaffolding. Listing
FilesystemMiddleware,SubAgentMiddlewareor the internal permission middleware inexcluded_middlewareraises aValueError. To hide their tools, useexcluded_toolsinstead. - Rewording is an option too.
tool_description_overridesreplaces a tool's description by name, which helps when one model keeps misreading a built-in.
To drop the task tool, the docs point to general_purpose_subagent=GeneralPurposeSubagentProfile(enabled=False) with no synchronous subagents passed, which leaves SubAgentMiddleware out cleanly. Subagents get their own part later in this series.
How many tools should a deep agent have?
There is no number in the docs, and I will not invent one. The working rule is the thesis of this post: every tool is a description the model reads every turn, so each one has to earn its place. In practice that means:
- Start from the built-ins and remove the ones the task cannot use (
executeon a non-sandbox backend,deleteon a read-only agent). - Prefer one tool with a
Literalargument over three near-duplicate tools. - Filter MCP
list_tools()output by name instead of passing a whole server through. - Keep docstrings short and specific; they are prompt, and they are billed as prompt.
- When a single agent genuinely needs many tools, that is a signal to split the work across subagents, each with its own small list.
A short tool list is not minimalism for its own sake. It is fewer wrong calls, fewer wasted tokens, and an agent whose choices you can predict.
FAQ
Do LangGraph deep agents support MCP?
Yes. Install langchain[mcp], open an MCPAdapter from langchain.mcp, call list_tools(), and pass the result to create_deep_agent(tools=...). HTTP, stdio and in-memory servers all work.
Should I still use langchain-mcp-adapters?
For new code, no. The LangChain MCP docs move MCP into the langchain.mcp namespace from langchain[mcp]>=1.4.0 and publish a migration guide from langchain-mcp-adapters. Note the new namespace is beta.
How do I remove a built-in tool like execute?
Register a HarnessProfile with excluded_tools=frozenset({"execute"}) for your provider or model. Do not try to exclude the filesystem or subagent middleware; that raises a ValueError.
Series: The harness · Quickstart · Tools and MCP (this post) · Virtual filesystem · Sandbox and permissions · Skills and memory · Context engineering · Subagents · Human-in-the-loop · Production
