Skip to content

Commit 0150c23

Browse files
author
RN
committed
fix(tools): Prevent crash in non-interactive environments
The MCPServerAdapter would call `click.confirm()` when the `mcp` package was missing, causing a crash in non-interactive environments. This commit replaces the interactive prompt with a non-interactive `ImportError` and adds robust lifecycle management and clearer documentation. A corresponding unit test is included to verify the new error handling.
1 parent 4cdc20a commit 0150c23

File tree

2 files changed

+116
-91
lines changed

2 files changed

+116
-91
lines changed

crewai_tools/adapters/mcp_adapter.py

Lines changed: 68 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55

66
from crewai.tools import BaseTool
77
from crewai_tools.adapters.tool_collection import ToolCollection
8-
"""
9-
MCPServer for CrewAI.
108

11-
12-
"""
139
logger = logging.getLogger(__name__)
1410

1511
if TYPE_CHECKING:
@@ -29,101 +25,90 @@
2925

3026

3127
class MCPServerAdapter:
32-
"""Manages the lifecycle of an MCP server and make its tools available to CrewAI.
28+
"""Manages the lifecycle of an MCP server and makes its tools available to CrewAI.
3329
34-
Note: tools can only be accessed after the server has been started with the
35-
`start()` method.
30+
This adapter handles starting and stopping the MCP server, converting its
31+
capabilities into CrewAI tools. It is best used as a context manager (`with`
32+
statement) to ensure resources are properly cleaned up.
3633
3734
Attributes:
38-
tools: The CrewAI tools available from the MCP server.
39-
40-
Usage:
41-
# context manager + stdio
42-
with MCPServerAdapter(...) as tools:
43-
# tools is now available
44-
45-
# context manager + sse
46-
with MCPServerAdapter({"url": "http://localhost:8000/sse"}) as tools:
47-
# tools is now available
48-
49-
# context manager with filtered tools
50-
with MCPServerAdapter(..., "tool1", "tool2") as filtered_tools:
51-
# only tool1 and tool2 are available
52-
53-
# manually stop mcp server
54-
try:
55-
mcp_server = MCPServerAdapter(...)
56-
tools = mcp_server.tools # all tools
57-
58-
# or with filtered tools
59-
mcp_server = MCPServerAdapter(..., "tool1", "tool2")
60-
filtered_tools = mcp_server.tools # only tool1 and tool2
61-
...
62-
finally:
63-
mcp_server.stop()
64-
65-
# Best practice is ensure cleanup is done after use.
66-
mcp_server.stop() # run after crew().kickoff()
35+
tools: A ToolCollection of the available CrewAI tools. Accessing this
36+
before the server is ready will raise a ValueError.
37+
38+
Example:
39+
# This is a minimal runnable example assuming an MCP server is running.
40+
# from your_agent_file import your_agent, your_task
41+
#
42+
# server_params = {"url": "http://localhost:6006/sse"}
43+
# with MCPServerAdapter(server_params) as mcp_tools:
44+
# your_agent.tools = mcp_tools
45+
# result = your_agent.kickoff(your_task)
6746
"""
6847

6948
def __init__(
7049
self,
7150
serverparams: StdioServerParameters | dict[str, Any],
7251
*tool_names: str,
7352
):
74-
"""Initialize the MCP Server
53+
"""Initialize and start the MCP Server.
7554
7655
Args:
77-
serverparams: The parameters for the MCP server it supports either a
78-
`StdioServerParameters` or a `dict` respectively for STDIO and SSE.
56+
serverparams: The parameters for the MCP server. This supports either a
57+
`StdioServerParameters` object for STDIO or a `dict` for SSE connections.
7958
*tool_names: Optional names of tools to filter. If provided, only tools with
8059
matching names will be available.
81-
8260
"""
83-
84-
super().__init__()
8561
self._adapter = None
8662
self._tools = None
8763
self._tool_names = list(tool_names) if tool_names else None
8864

8965
if not MCP_AVAILABLE:
90-
import click
91-
92-
if click.confirm(
93-
"You are missing the 'mcp' package. Would you like to install it?"
94-
):
95-
import subprocess
96-
97-
try:
98-
subprocess.run(["uv", "add", "mcp crewai-tools[mcp]"], check=True)
99-
100-
except subprocess.CalledProcessError:
101-
raise ImportError("Failed to install mcp package")
102-
else:
103-
raise ImportError(
104-
"`mcp` package not found, please run `uv add crewai-tools[mcp]`"
105-
)
66+
msg = (
67+
"❌ MCP is not available. The 'mcp' package, a required dependency, "
68+
"must be installed for MCPServerAdapter to work."
69+
)
70+
logger.critical(msg)
71+
raise ImportError(
72+
"`mcp` package not found. Please install it with:\n"
73+
" pip install mcp crewai-tools[mcp]"
74+
)
10675

10776
try:
10877
self._serverparams = serverparams
10978
self._adapter = MCPAdapt(self._serverparams, CrewAIAdapter())
11079
self.start()
111-
11280
except Exception as e:
81+
logger.exception("Failed to initialize MCP Adapter during __init__.")
11382
if self._adapter is not None:
11483
try:
11584
self.stop()
11685
except Exception as stop_e:
117-
logger.error(f"Error during stop cleanup: {stop_e}")
86+
logger.error(f"Error during post-failure cleanup: {stop_e}")
11887
raise RuntimeError(f"Failed to initialize MCP Adapter: {e}") from e
11988

12089
def start(self):
12190
"""Start the MCP server and initialize the tools."""
91+
if not self._adapter:
92+
raise RuntimeError("Cannot start MCP server: Adapter is not initialized.")
93+
if self._tools:
94+
logger.debug("MCP server already started.")
95+
return
12296
self._tools = self._adapter.__enter__()
12397

12498
def stop(self):
125-
"""Stop the MCP server"""
126-
self._adapter.__exit__(None, None, None)
99+
"""Stop the MCP server and release all associated resources.
100+
101+
This method is idempotent; calling it multiple times has no effect.
102+
"""
103+
if not self._adapter:
104+
logger.debug("stop() called but adapter is already stopped.")
105+
return
106+
107+
try:
108+
self._adapter.__exit__(None, None, None)
109+
finally:
110+
self._tools = None
111+
self._adapter = None
127112

128113
@property
129114
def tools(self) -> ToolCollection[BaseTool]:
@@ -133,11 +118,11 @@ def tools(self) -> ToolCollection[BaseTool]:
133118
ValueError: If the MCP server is not started.
134119
135120
Returns:
136-
The CrewAI tools available from the MCP server.
121+
A ToolCollection of the available CrewAI tools.
137122
"""
138123
if self._tools is None:
139124
raise ValueError(
140-
"MCP server not started, run `mcp_server.start()` first before accessing `tools`"
125+
"MCP tools are not available. The server may be stopped or initialization failed."
141126
)
142127

143128
tools_collection = ToolCollection(self._tools)
@@ -146,12 +131,25 @@ def tools(self) -> ToolCollection[BaseTool]:
146131
return tools_collection
147132

148133
def __enter__(self):
149-
"""
150-
Enter the context manager. Note that `__init__()` already starts the MCP server.
151-
So tools should already be available.
152-
"""
134+
"""Enter the context manager, returning the initialized tools."""
153135
return self.tools
154136

155137
def __exit__(self, exc_type, exc_value, traceback):
156-
"""Exit the context manager."""
157-
return self._adapter.__exit__(exc_type, exc_value, traceback)
138+
"""Exit the context manager, stop the server, and do not suppress exceptions."""
139+
self.stop()
140+
return False # Ensures any exceptions that occurred are re-raised.
141+
142+
def __del__(self):
143+
"""
144+
Finalizer to attempt cleanup if the user forgets to call stop() or use a
145+
context manager.
146+
147+
Note: This is a fallback and should not be relied upon, as Python does
148+
not guarantee __del__ will always be called on object destruction.
149+
"""
150+
if self._adapter:
151+
logger.warning(
152+
"MCPServerAdapter was not cleanly shut down. Please use a "
153+
"context manager (`with` statement) or call .stop() explicitly."
154+
)
155+
self.stop()

0 commit comments

Comments
 (0)