L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Déploiement et opérations · SKILL.md

connect-tools-and-mcp

connect-tools-and-mcp

Giving crewAI agents tools and MCP servers on crewai 1.15.x: custom BaseTool with args_schema and @tool, which crewai_tools names really exist, tool caching, max_usage_count, ToolFailure, Agent(mcps=[...]) string and MCPServerStdio/HTTP/SSE forms, rewritten MCP tool names, MCPServerAdapter, timeouts, and what works on a hosted deployment. Use when writing o…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: connect-tools-and-mcp description: "Giving crewAI agents tools and MCP servers on crewai 1.15.x: custom BaseTool with args_schema and @tool, which crewai_tools names really exist, tool caching, max_usage_count, ToolFailure, Agent(mcps=[...]) string and MCPServerStdio/HTTP/SSE forms, rewritten MCP tool names, MCPServerAdapter, timeouts, and what works on a hosted deployment. Use when writing or debugging a tool or MCP connection, when you see 'cannot import name BaseTool from crewai_tools', 'You are missing the mcp package', 'MCPConnectionError', 'Operation timed out after 30 seconds', 'reached its usage limit', 'Invalid MCP reference', 'Anthropic function name ... must start with a letter', an agent that never calls an MCP tool, mcps=[...] that yields no tools (for example an https:// string under akickoff or on CrewAI AMP), or a stdio MCP server that works locally but not after deploy."

Connect 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 needUseSection
A quick Python function as a tool@tool from crewai.tools3
A tool with config, state, or a documented input schemaBaseTool subclass with args_schema3
A prebuilt search/scrape/file toolcrewai_tools - but import it once to prove the name exists5
A remote MCP serverAgent(mcps=[MCPServerHTTP(...)]) - not an https:// string if the crew is deployed or uses akickoff()6, 8
An MCP server that ships with your codeAgent(mcps=[MCPServerStdio(...)]) - also works on CrewAI AMP when the server is in the package6, 10
An integration connected in your CrewAI AMP accountmcps=["<slug>"] or "<slug>#<tool>"6
Bare MCP tool names or a manual start/stop lifecycleMCPServerAdapter (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 wroteCurrent formWhat happens with the old form
from crewai_tools import BaseToolfrom crewai.tools import BaseToolImportError: cannot import name 'BaseTool' from 'crewai_tools'
from crewai_tools import toolfrom crewai.tools import toolImportError: cannot import name 'tool' from 'crewai_tools'
from crewai_tools import CodeInterpreterToola sandbox tool: E2BPythonTool, DaytonaPythonToolImportError - removed
from crewai_tools import ZapierActionsAdapterfrom crewai_tools.adapters.zapier_adapter import ZapierActionsAdapter (or top-level ZapierActionTools)ImportError at top level
from crewai.mcp import ... for the adapterfrom crewai_tools import MCPServerAdapteradapter 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:

RuleFailure 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 docstringValueError: Function must have a docstring at decoration time
Write the description for the LLM: what it does and when to use itThe 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 stringAn 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):

FieldEffect (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: boolDecides 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

SettingDefault in 1.15.22-1.15.23Effect
Crew(cache=...)FalseOpt-in. With True, a repeat call with identical arguments returns the first result without running the tool
Agent(cache=...)TrueOnly permits participation; False opts that agent out even when the crew caches
BaseTool(cache_function=...)always cacheReturn 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
CodeInterpreterToolremoved - use E2BPythonTool / DaytonaPythonTool
BedrockKBRetrieverBedrockKBRetrieverTool
GitHubSearchTool, GithubToolGithubSearchTool
ArxivToolArxivPaperTool
ComposioToolSetComposioTool
GoogleSearchTool, WebSearchTool, DuckDuckGoSearchToolSerperDevTool, BraveSearchTool, TavilySearchTool, EXASearchTool
PDFTextWritingTool, PGSearchToolnone
WikipediaTool, PythonREPLTool, ShellTool, CalculatorTool, HumanTool, LangChainTool, SeleniumToolnone (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.

FormRule (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" stringMust 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" stringAn 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 slugValidationError: ... 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 namesMatched 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.

SourceName 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.pyopt_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:// stringmcp_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
MCPServerAdapterlookup_stock (bare)

Consequences:

  • Do not write "call the lookup_stock tool" 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" called python_mcp_server_py_lookup_stock), but a model on the text ReAct path fails with Action 'lookup_stock' don't exist, and a hashed name leaves only the description to go on.
  • Keep stdio command/args short ("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 ./data hashed list_allowed_directories to ..._data_list_4ab656ee; use MCPServerAdapter when bare names matter.
  • In code, identify a resolved native MCP tool by tool.original_tool_name, not by tool.name.

8. Timeouts, retries and failure behaviour

PathConnectPer tool callRetriesWhen the server is down
MCPServerStdio/HTTP/SSE config and AMP slugs30 s30 s3 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 insteadkickoff raises MCPConnectionError
"https://..." string10 s, 15 s discovery60 s3 attempts (a 65 s tool ran 3 times, ~198 s) and then returns the error as a stringagent runs with no MCP tools, no error
MCPServerAdapterconnect_timeout= (default 30)no crewai timeout (a 35 s call completed)noneconstructor 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 with asyncio.run(). Inside a running event loop that fails and the agent gets zero tools with no error (only RuntimeWarning: coroutine 'MCPToolResolver._get_mcp_tool_schemas_async' was never awaited). Seen live with await 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 as MCPServerHTTP(url=...) worked in both places. Sync crew.kickoff(), including from a sync Flow step, resolves strings fine.
  • An MCP result with isError: true reaches the agent as text. It is recorded as a mcp_error tool failure, and tool_failure_policy="raise" turns it into ToolExecutionFailedError.

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 crewResult
MCPServerHTTP(url="https://<public host>/mcp")Works - the model called <host>_mcp_<tool>
"https://<public host>/mcp#tool" stringZero 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.executableWorks (tool name hashed, section 7)
MCPServerStdio(..., env={"X": os.environ["X"]}) with X a deployment env varServer sees X; without env= it does not
MCPServerAdapter(StdioServerParameters(...)) with crewai-tools[mcp] in pyproject.tomlWorks, 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 accountZero 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 with python -m <pkg>.server) and its Python dependencies in pyproject.toml/uv.lock; a laptop path or a Node/npx server is not there.
  • For remote servers use MCPServerHTTP(url="https://<host>/mcp") (or MCPServerSSE), never an https:// 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>/mcp returns 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 headers read from environment variables (os.environ["ACME_MCP_TOKEN"]), never in the URL string or in code. A stdio server gets deployment env vars only through env=.

Common mistakes

SymptomCauseFix
cannot import name 'BaseTool' from 'crewai_tools'0.x import pathfrom crewai.tools import BaseTool, tool
Can't instantiate abstract class ... '_run'Only _arun definedDefine _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 toolPrompt names the bare tool (lookup_stock) but the LLM sees python_..._lookup_stockDescribe 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 toolsSame, but https strings are not validatedOne ref per tool
ValidationError ... Invalid MCP reference: 'http://localhost:8000/mcp'Strings must be https:// or a slugMCPServerHTTP(url="http://localhost:8000/mcp")
Anthropic function name '127_0_0_1_...' must start with a letter or underscoreMCP server URL uses an IP addressUse a hostname (localhost)
https:// string MCP gives no tools under akickoff() or on AMP; the answer invents tool callsString refs cannot resolve inside a running event loopMCPServerHTTP(url=...)
GET /status keeps failing or the run never completesThe 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 nothingFilter used the server's camelCase nameUse 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 repeatedNative MCP call exceeded 30 s and was retriedKeep calls short and idempotent
Crew "succeeds" but the answer says the tool erroredTool exceptions are fed to the agent, not raisedReturn ToolFailure; set tool_failure_policy="raise"; check tool_failures
Stale results after enabling Crew(cache=True)Live-data tool cachedcache_function=lambda a, r: False or Agent(cache=False)
Repeated identical calls now hit the API every timeCrew cache is opt-in in 1.15.xCrew(cache=True)
Second kickoff of the same @CrewBase instance: Event loop is closedAdapter stopped after the first kickoffNew crew instance per kickoff
stdio MCP server says its API key is missing although it is in your .envstdio servers get a minimal environment, not yoursMCPServerStdio(..., env={"X_API_KEY": os.environ["X_API_KEY"]})
Works locally, fails after deploystdio server file or command (npx, a laptop path) not in the deploymentShip the server inside the package, or serve it over HTTP at a stable URL

Checklist

  • BaseTool/tool imported from crewai.tools; every crewai_tools name imported once to prove it exists
  • Custom tools define _run, have an args_schema (or typed @tool signature) and an LLM-oriented description
  • Failures returned as ToolFailure; tool_failure_policy chosen 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 via MCPServerHTTP, not a string; hostnames, not IP addresses
  • No https:// string refs in deployed crews or with akickoff() - use MCPServerHTTP
  • 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 @CrewBase instance per kickoff when using mcp_server_params

References

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
Skills enregistrées