About connect-tools-and-mcp
A guide for developers equipping CrewAI agents with custom tools, crewai_tools, and MCP connections. Verified against CrewAI 1.15.x, it focuses on API differences and runtime pitfalls that can otherwise go unnoticed.
Key features
- 01Choose custom-tool and MCP integration methods
- 02Configure stdio, HTTP, and SSE connections
- 03Understand tool naming and filtering rules
- 04Review caching and tool-failure handling
- 05Check available crewai_tools names
Use cases
- 01Connect local or remote MCP servers to CrewAI agents
- 02Troubleshoot tool imports or execution
- 03Configure tool caching and failure policies
MCP support
The skill explains how CrewAI agents connect to MCP servers, including native configuration and MCPServerAdapter, and covers transports and tool filtering.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ crewaiinc/ skills --skill connect-tools-and-mcpAfter installing, type/connect-tools-and-mcpin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/crewai-tools-names.mdcustom-tools.mdmcp-connections.mdDetails
From the project READMEConnect Tools and MCP Servers to CrewAI Agents
Give an agent working tools - custom Python, prebuilt crewai_tools, or MCP servers - without the 0.x-era mistakes that fail silently.
Verified against crewai 1.15.22 and 1.15.23 on 2026-10-01.
Live-tested on CrewAI AMP and real LLMs on 2026-10-01.
Run crewai version first; if the major/minor differs from 1.15, re-verify version-sensitive rows with the ask-docs skill before trusting them.
1. Pick the mechanism
| You need | Use | Section |
|---|---|---|
| A quick Python function as a tool | @tool from crewai.tools | 3 |
| A tool with config, state, or a documented input schema | BaseTool subclass with args_schema | 3 |
| A prebuilt search/scrape/file tool | crewai_tools - but import it once to prove the name exists | 5 |
| A remote MCP server | Agent(mcps=[MCPServerHTTP(...)]) - not an https:// string if the crew is deployed or uses akickoff() | 6, 8 |
| An MCP server that ships with your code | Agent(mcps=[MCPServerStdio(...)]) - also works on CrewAI AMP when the server is in the package | 6, 10 |
| An integration connected in your CrewAI AMP account | mcps=["<slug>"] or "<slug>#<tool>" | 6 |
| Bare MCP tool names or a manual start/stop lifecycle | MCPServerAdapter (needs crewai-tools[mcp]) | 9 |
mcp is already a core dependency of crewai 1.15.x - native mcps= needs no extra install.
2. Imports
| You probably wrote | Current form | What happens with the old form |
|---|---|---|
from crewai_tools import BaseTool | from crewai.tools import BaseTool | ImportError: cannot import name 'BaseTool' from 'crewai_tools' |
from crewai_tools import tool | from crewai.tools import tool | ImportError: cannot import name 'tool' from 'crewai_tools' |
from crewai_tools import CodeInterpreterTool | a sandbox tool: E2BPythonTool, DaytonaPythonTool | ImportError - removed |
from crewai_tools import ZapierActionsAdapter | from crewai_tools.adapters.zapier_adapter import ZapierActionsAdapter (or top-level ZapierActionTools) | ImportError at top level |
from crewai.mcp import ... for the adapter | from crewai_tools import MCPServerAdapter | adapter lives in crewai_tools; native configs live in crewai.mcp |
from crewai.tools import BaseTool, tool, ToolFailure, ToolExecutionFailedError
from crewai.mcp import (
MCPServerStdio, MCPServerHTTP, MCPServerSSE,
create_static_tool_filter, create_dynamic_tool_filter,
)
from crewai.mcp.exceptions import MCPConnectionError
3. Custom tools
from pydantic import BaseModel, Field
from crewai.tools import BaseTool, tool
class PriceInput(BaseModel):
sku: str = Field(..., description="Product SKU, e.g. 'A-100'")
currency: str = Field("EUR", description="ISO currency code")
class PriceTool(BaseTool):
name: str = "price_lookup"
description: str = "Return the unit price for a SKU. Use before quoting a price."
args_schema: type[BaseModel] = PriceInput
def _run(self, sku: str, currency: str = "EUR") -> str:
return f"{sku} costs 9.99 {currency}"
@tool("Unit Converter")
def convert(km: float) -> str:
"""Convert kilometres to miles."""
return f"{km * 0.621371:.1f} mi"
print(PriceTool().run(sku="A-100")) # args are validated against PriceInput first
print(convert.run(km=10))
Rules that prevent real failures:
| Rule | Failure it prevents |
|---|---|
Always define _run, even for an async tool | _run is abstract: TypeError: Can't instantiate abstract class ... without an implementation for abstract method '_run' |
Put async code in async def _run(...) | Agents execute tools through _run in both kickoff() and akickoff(); an _arun override is only used when you call tool.arun() yourself |
Give @tool functions a docstring | ValueError: Function must have a docstring at decoration time |
| Write the description for the LLM: what it does and when to use it | The agent picks tools by description; MCP tools in particular have opaque names (section 7) |
| Expect the displayed name to be sanitized | @tool("Unit Converter") is offered to the LLM as unit_converter (seen in a live run with claude-haiku-4-5) |
Return a ToolFailure instead of an error string | An error string looks like data; a ToolFailure is recorded in TaskOutput.tool_failures and can abort the run (below) |
Per-tool options (all on BaseTool and accepted by @tool(...) where noted):
| Field | Effect (verified) |
|---|---|
max_usage_count=N (also @tool("x", max_usage_count=N)) | After N runs within one task the agent gets Tool '...' has reached its usage limit of N times and cannot be used anymore. The next task or kickoff gets a fresh allowance, so enforce "once per run" side effects yourself. 0 or negative is a ValidationError |
result_as_answer=True (also on @tool) | The tool's return value becomes the task output; no further LLM call |
cache_function=lambda args, result: bool | Decides per call whether the result may be cached (section 4) |
tool_failure_policy="ignore" | "warn" | "raise" | Also on Agent, Task and Crew; default warn. raise aborts with ToolExecutionFailedError |
Failures: a tool that raises does not fail the crew. The exception text is fed back to the agent and recorded as a tool failure, and the crew finishes "successfully". When downstream code must know, return ToolFailure(...) and set tool_failure_policy="raise", or inspect result.tasks_output[i].tool_failures. Full examples: references/custom-tools.md.
Task(tools=[...]) replaces the agent's tools for that task; it does not add to them.
4. Tool result caching
| Setting | Default in 1.15.22-1.15.23 | Effect |
|---|---|---|
Crew(cache=...) | False | Opt-in. With True, a repeat call with identical arguments returns the first result without running the tool |
Agent(cache=...) | True | Only permits participation; False opts that agent out even when the crew caches |
BaseTool(cache_function=...) | always cache | Return False to keep a result out of the cache |
Through crewai 1.15.2 the crew cached tool results by default; from 1.15.3 it is opt-in. If you relied on the old behaviour, set Crew(cache=True) explicitly. Never enable it for live-data or state-changing tools unless their cache_function returns False.
A separate guard is not caching: if the agent repeats the exact same call twice in a row, crewai refuses the second call ("I tried reusing the same input...") whether or not caching is on.
5. crewai_tools: names that exist
crewai-tools 1.15.22-1.15.23 exports 118 names. Import a tool before you write code around it. Models routinely invent these:
| Hallucinated name (ImportError) | Real 1.15.22-1.15.23 name |
|---|---|
CodeInterpreterTool | removed - use E2BPythonTool / DaytonaPythonTool |
BedrockKBRetriever | BedrockKBRetrieverTool |
GitHubSearchTool, GithubTool | GithubSearchTool |
ArxivTool | ArxivPaperTool |
ComposioToolSet | ComposioTool |
GoogleSearchTool, WebSearchTool, DuckDuckGoSearchTool | SerperDevTool, BraveSearchTool, TavilySearchTool, EXASearchTool |
PDFTextWritingTool, PGSearchTool | none |
WikipediaTool, PythonREPLTool, ShellTool, CalculatorTool, HumanTool, LangChainTool, SeleniumTool | none (SeleniumScrapingTool exists) - write a custom tool |
Many tools need a third-party package. When it is missing, constructing the tool asks You are missing the '<pkg>' package. Would you like to install it? [y/N] (verified for EXASearchTool, TavilySearchTool, FirecrawlSearchTool, SeleniumScrapingTool). With a terminal attached that blocks forever; with stdin closed it raises click.exceptions.Abort. Install the extra up front, e.g. uv add "crewai-tools[exa-py]". Prebuilt tools are offered to the LLM under their sanitized display name, not the class name: DirectoryReadTool appears as list_files_in_directory, FileReadTool as read_a_files_content. RAG tools (WebsiteSearchTool, PDFSearchTool, ...) fail at construction without an embedder key. Full list and extras: references/crewai-tools-names.md.
6. Native Agent(mcps=[...])
from crewai import Agent
from crewai.mcp import MCPServerHTTP, MCPServerSSE, MCPServerStdio, create_static_tool_filter
agent = Agent(
role="Inventory analyst",
goal="Answer stock questions",
backstory="Careful with numbers.",
mcps=[
MCPServerHTTP(url="https://mcp.example.com/mcp",
headers={"Authorization": "Bearer <your-key>"}),
MCPServerSSE(url="https://legacy.example.com/sse"),
MCPServerStdio(command="uvx", args=["mcp-server-time"],
tool_filter=create_static_tool_filter(allowed_tool_names=["get_current_time"])),
"https://docs.example.com/mcp#search_docs", # https string, one tool (sync kickoff only - section 8)
"acme#lookup_stock", # AMP-connected integration, one tool
"acme#list_warehouses", # second tool = second ref
],
)
String refs are validated at construction; nothing connects until a task runs.
| Form | Rule (all verified) |
|---|---|
MCPServerStdio(command, args, env, tool_filter, cache_tools_list) | Spawns a subprocess where the crew runs. The server does not inherit your environment: it gets only HOME, LOGNAME, PATH, SHELL, TERM, USER plus env=, so pass its API keys in env explicitly. No cwd or timeout field |
MCPServerHTTP(url, headers, streamable=True, tool_filter, cache_tools_list) | Streamable HTTP; http:// and https:// both work. Use a hostname, not an IP: http://127.0.0.1:8765/mcp makes tool names start with a digit and Anthropic models reject them (ValueError: Anthropic function name '127_0_0_1_8765_mcp_lookup_stock' must start with a letter or underscore); http://localhost:8765/mcp works |
MCPServerSSE(url, headers, tool_filter, cache_tools_list) | Legacy SSE servers (usually a /sse path) |
"https://host/path" string | Must start with https://. Optional #tool keeps one tool. Resolves to zero tools, silently, inside a running event loop - await crew.akickoff() and crews deployed on CrewAI AMP (section 8) |
"slug" / "slug#tool" string | An integration connected in a CrewAI AMP account; legacy "crewai-amp:slug" also works. A slug the account has not connected gives zero tools and the run continues (seen on AMP) |
"http://localhost:8000/mcp" or any string that is neither https:// nor a slug | ValidationError: ... Invalid MCP reference: 'http://...'. String references must be an 'https://' URL or a valid slug when the Agent is built. Use MCPServerHTTP for http:// |
"slug#a,b" | Same Invalid MCP reference ValidationError. One ref per tool |
"https://...#a,b" | Accepted, but selects zero tools (# takes one name). One ref per tool |
tool_filter names | Matched against the sanitized tool name: a server tool getForecast must be listed as get_forecast |
Full examples for each transport, dynamic filters and the AMP form: references/mcp-connections.md.
7. MCP tool names are rewritten - never hard-code them
Native MCP tools are offered to the LLM as <server name>_<tool name>, then sanitized: lowercase, non-alphanumerics become _, camelCase is split, max 64 characters.
| Source | Name the LLM sees for tool lookup_stock |
|---|---|
MCPServerStdio(command="python", args=["servers/inventory.py"]) | python_servers_inventory_py_lookup_stock |
MCPServerStdio with absolute paths, e.g. /opt/app/.venv/bin/python + /opt/app/servers/inventory_server.py | opt_app_venv_bin_python_opt_app_servers_inventory_serve_a87662e9 - over 64 chars, so it is truncated and hashed and the tool name is gone. command=sys.executable does the same, locally and on AMP |
MCPServerHTTP(url="http://localhost:8765/mcp") | localhost_8765_mcp_lookup_stock (an IP host would start the name with a digit - section 6) |
MCPServerHTTP(url="https://mcp.example.com/mcp") or the same https:// string | mcp_example_com_mcp_lookup_stock |
"acme#lookup_stock" (AMP) | built from the server URL the account returns, not from acme - only the _lookup_stock suffix is stable |
MCPServerAdapter | lookup_stock (bare) |
Consequences:
- Do not write "call the
lookup_stocktool" in a task description or backstory. Describe the capability ("check stock levels with the inventory tool"), or derive the exact name at runtime. A native function-calling model usually copes (claude-haiku-4-5 told "call the lookup_stock tool" calledpython_mcp_server_py_lookup_stock), but a model on the text ReAct path fails withAction 'lookup_stock' don't exist, and a hashed name leaves only the description to go on. - Keep stdio
command/argsshort ("uvx","npx", a relative script path) so names stay readable and under 64 characters. Absolute paths also put your filesystem layout into every prompt. A scoped npx package already uses about 45 characters:npx -y @modelcontextprotocol/server-filesystem ./datahashedlist_allowed_directoriesto..._data_list_4ab656ee; useMCPServerAdapterwhen bare names matter. - In code, identify a resolved native MCP tool by
tool.original_tool_name, not bytool.name.
8. Timeouts, retries and failure behaviour
| Path | Connect | Per tool call | Retries | When the server is down |
|---|---|---|---|---|
MCPServerStdio/HTTP/SSE config and AMP slugs | 30 s | 30 s | 3 attempts on timeout - the tool runs again each time. Live run: a 35 s tool ran 3 times (~99 s); the agent got Error executing tool: Error executing MCP tool slow_report: Operation timed out after 30 seconds as its observation, a tool failure was recorded and the crew finished. A direct tool.run() raises RuntimeError instead | kickoff raises MCPConnectionError |
"https://..." string | 10 s, 15 s discovery | 60 s | 3 attempts (a 65 s tool ran 3 times, ~198 s) and then returns the error as a string | agent runs with no MCP tools, no error |
MCPServerAdapter | connect_timeout= (default 30) | no crewai timeout (a 35 s call completed) | none | constructor raises RuntimeError: Failed to initialize MCP Adapter |
In 1.15.22-1.15.23 the native timeouts are module constants. No field on MCPServerStdio, MCPServerHTTP, MCPServerSSE or Agent changes them. So:
- Keep MCP tool calls well under 30 s. Make them idempotent, because a timed-out call is re-sent. For long jobs, return a job id from one tool and poll with another.
- Prefer config objects to
https://strings in production: they fail loudly at kickoff instead of silently dropping tools. https://strings discover tools withasyncio.run(). Inside a running event loop that fails and the agent gets zero tools with no error (onlyRuntimeWarning: coroutine 'MCPToolResolver._get_mcp_tool_schemas_async' was never awaited). Seen live withawait crew.akickoff()and on a CrewAI AMP deployment - the agent runs without the tools, and its answer may describe tool calls it never made. The same server asMCPServerHTTP(url=...)worked in both places. Synccrew.kickoff(), including from a sync Flow step, resolves strings fine.- An MCP result with
isError: truereaches the agent as text. It is recorded as amcp_errortool failure, andtool_failure_policy="raise"turns it intoToolExecutionFailedError.
9. MCPServerAdapter and @CrewBase mcp_server_params
MCPServerAdapter needs the extra: uv add "crewai-tools[mcp]" (it installs mcpadapt; mcp itself is already present). Without the extra, constructing it asks You are missing the 'mcp' package. Would you like to install it? [y/N]. Under a terminal that blocks forever; with stdin closed (typical CI) it raises click.exceptions.Abort. Install the extra explicitly.
from mcp import StdioServerParameters
from crewai_tools import MCPServerAdapter
params = StdioServerParameters(command="python", args=["mcp_server.py"])
with MCPServerAdapter(params, "lookup_stock", connect_timeout=60) as tools:
print([t.name for t in tools]) # ['lookup_stock'] - bare names, filtered by name
# build agents with tools=tools and kick off INSIDE the with-block
The server starts in the constructor and stops when the with block exits. Tools used after that fail. For a remote server pass a dict and name the transport: {"url": "https://<host>/mcp", "transport": "streamable-http"}. Without "transport" the adapter speaks SSE, and against a streamable-HTTP /mcp endpoint it fails with Couldn't connect to the MCP server after <connect_timeout> seconds.
In a @CrewBase class, set mcp_server_params (and optionally mcp_connect_timeout, default 30) and call self.get_mcp_tools("name", ...) in an @agent method. crewai stops that adapter after every kickoff. Calling .crew().kickoff() a second time on the same instance made every MCP call return Event loop is closed, yet the crew still "succeeded". Build a fresh instance per run: InventoryCrew().crew().kickoff().
10. Hosted deployments: what works on CrewAI AMP
Measured on a CrewAI AMP deployment (ZIP deploy, crewai 1.15.23, anthropic/claude-haiku-4-5), one kickoff per form:
| Form in the deployed crew | Result |
|---|---|
MCPServerHTTP(url="https://<public host>/mcp") | Works - the model called <host>_mcp_<tool> |
"https://<public host>/mcp#tool" string | Zero tools, run SUCCESS - the model made up a tool call in its answer (section 8) |
MCPServerStdio(command="python", args=["-m", "<pkg>.inv_server"]), server module inside src/<pkg>/ | Works - a stdio server shipped in the package runs inside the deployment |
Same with command=sys.executable | Works (tool name hashed, section 7) |
MCPServerStdio(..., env={"X": os.environ["X"]}) with X a deployment env var | Server sees X; without env= it does not |
MCPServerAdapter(StdioServerParameters(...)) with crewai-tools[mcp] in pyproject.toml | Works, bare tool names |
MCPServerStdio pointing at a file that is not in the deployment (a laptop path) | Run fails with MCPConnectionError |
MCPServerStdio(command="npx", ...) | Run fails the same way - the deployment only has what your Python project installs |
MCPServerHTTP at a wrong path (HTTP 404) | Run fails the same way |
"<slug>#tool" not connected in the account | Zero tools, run SUCCESS |
How a failed MCP connection looks from the API: for each MCPConnectionError run above, GET /status/{kickoff_id} answered HTTP 500 Internal Server Error (plain text) on every poll, minutes later too, instead of a JSON FAILED state. A plain exception in the same crew gave the documented shape: {"state": "FAILED", "status": "ValueError: ..."}. Treat a persistent 500 from /status as "the run failed", and check the MCP connection first.
Rules:
- A stdio server runs as a child process wherever the crew runs. Ship it inside the package (
src/<pkg>/server.py, started withpython -m <pkg>.server) and its Python dependencies inpyproject.toml/uv.lock; a laptop path or a Node/npx server is not there. - For remote servers use
MCPServerHTTP(url="https://<host>/mcp")(orMCPServerSSE), never anhttps://string. Point deployments at a stable hostname you control, require auth on it, and do not leave temporary tunnels running. - Health check: a bare
curl -s -o /dev/null -w "%{http_code}" https://<host>/mcpreturns 406 from a healthy streamable-HTTP MCP endpoint (seen on a public server and locally). 404 means wrong path;000/connection refused means it is down. - Put tokens in
headersread from environment variables (os.environ["ACME_MCP_TOKEN"]), never in the URL string or in code. A stdio server gets deployment env vars only throughenv=.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
cannot import name 'BaseTool' from 'crewai_tools' | 0.x import path | from crewai.tools import BaseTool, tool |
Can't instantiate abstract class ... '_run' | Only _arun defined | Define _run (it may be async def) |
Action 'lookup_stock' don't exist, these are the only available Actions: ..., or the agent never uses the MCP tool | Prompt names the bare tool (lookup_stock) but the LLM sees python_..._lookup_stock | Describe the capability; derive names at runtime |
ValidationError ... Invalid MCP reference: 'github#a,b' | # takes one tool name | ["github#a", "github#b"] |
mcps=["https://...#a,b"] gives no tools | Same, but https strings are not validated | One ref per tool |
ValidationError ... Invalid MCP reference: 'http://localhost:8000/mcp' | Strings must be https:// or a slug | MCPServerHTTP(url="http://localhost:8000/mcp") |
Anthropic function name '127_0_0_1_...' must start with a letter or underscore | MCP server URL uses an IP address | Use a hostname (localhost) |
https:// string MCP gives no tools under akickoff() or on AMP; the answer invents tool calls | String refs cannot resolve inside a running event loop | MCPServerHTTP(url=...) |
GET /status keeps failing or the run never completes | The run may have hit MCPConnectionError (stdio command or file missing, wrong URL) | Run the crew locally to see the error; fix the MCP connection |
tool_filter keeps nothing | Filter used the server's camelCase name | Use the sanitized name (get_forecast) |
| Run hangs at "Would you like to install it? [y/N]" | Missing optional package (crewai-tools[mcp], exa-py, ...) | Install the extra before running |
| Tool ran 3 times / side effect repeated | Native MCP call exceeded 30 s and was retried | Keep calls short and idempotent |
| Crew "succeeds" but the answer says the tool errored | Tool exceptions are fed to the agent, not raised | Return ToolFailure; set tool_failure_policy="raise"; check tool_failures |
Stale results after enabling Crew(cache=True) | Live-data tool cached | cache_function=lambda a, r: False or Agent(cache=False) |
| Repeated identical calls now hit the API every time | Crew cache is opt-in in 1.15.x | Crew(cache=True) |
Second kickoff of the same @CrewBase instance: Event loop is closed | Adapter stopped after the first kickoff | New crew instance per kickoff |
stdio MCP server says its API key is missing although it is in your .env | stdio servers get a minimal environment, not yours | MCPServerStdio(..., env={"X_API_KEY": os.environ["X_API_KEY"]}) |
| Works locally, fails after deploy | stdio server file or command (npx, a laptop path) not in the deployment | Ship the server inside the package, or serve it over HTTP at a stable URL |
Checklist
-
BaseTool/toolimported fromcrewai.tools; everycrewai_toolsname imported once to prove it exists - Custom tools define
_run, have anargs_schema(or typed@toolsignature) and an LLM-oriented description - Failures returned as
ToolFailure;tool_failure_policychosen deliberately -
Crew(cache=...)set explicitly; live-data tools excluded from caching - Optional packages and
crewai-tools[mcp]installed up front, so no interactive prompt can block - One
"slug#tool"ref per tool;http://servers viaMCPServerHTTP, not a string; hostnames, not IP addresses - No
https://string refs in deployed crews or withakickoff()- useMCPServerHTTP - No prompt or code depends on a bare MCP tool name; stdio commands kept short
- stdio servers get their keys through
env=(they do not inherit the crew's environment) - MCP tool calls finish well under 30 s and are idempotent
- Deployed crews use HTTP/SSE MCP servers at a stable URL that returns 406 on a bare GET, or stdio servers shipped inside the package
- A fresh
@CrewBaseinstance per kickoff when usingmcp_server_params
References
- Custom Tools - BaseTool,
@tool, async, ToolFailure and failure policy, caching, usage limits, with runnable examples - MCP Connections - every
mcps=form, filters, name derivation, timeouts, MCPServerAdapter,@CrewBase, deployment - crewai_tools Names - the full 1.15.22-1.15.23 export list, hallucinated names, extras and construction behaviour
- Public docs: https://docs.crewai.com/en/concepts/tools and https://docs.crewai.com/en/mcp/overview
For related skills:
- check-crewai-api - current Agent/Task/Crew API and imports beyond tools
- design-agent - choosing how many tools an agent gets and writing role/goal/backstory
- test-crewai-project - driving tool calls offline with a stub LLM
- deploy-to-amp - getting a project with MCP connections onto CrewAI AMP
- ask-docs - query the live docs when the installed version differs