mcp server proposal (#267)

* mcp server proposal

* Refactor MCP server implementation: rename MCPServer to MCPServerAdapter and update usage examples. Adjust error message for optional dependencies installation.

* Update MCPServerAdapter usage examples to remove unnecessary parameters in context manager instantiation.

* Refactor MCPServerAdapter to move optional dependency imports inside the class constructor, improving error handling for missing dependencies.

* Enhance MCPServerAdapter by adding type hinting for server parameters and improving error handling during server startup. Optional dependency imports are now conditionally loaded, ensuring clearer error messages for missing packages.

* Refactor MCPServerAdapter to improve error handling for missing 'mcp' package. Conditional imports are now used, prompting users to install the package if not found, enhancing user experience during server initialization.

* Refactor MCPServerAdapter to ensure proper cleanup after usage. Removed redundant exception handling and ensured that the server stops in a finally block, improving resource management.

* add documentation

* fix typo close -> stop

* add tests and fix double call with context manager

* Enhance MCPServerAdapter with logging capabilities and improved error handling during initialization. Added logging for cleanup errors and refined the structure for handling missing 'mcp' package dependencies.

---------

Co-authored-by: lorenzejay <lorenzejaytech@gmail.com>
This commit is contained in:
Guillaume Raille
2025-04-16 19:18:07 +02:00
committed by GitHub
parent 8cbdaeaff5
commit a270742319
4 changed files with 367 additions and 0 deletions

117
README.md
View File

@@ -66,6 +66,123 @@ def my_custom_function(input):
---
## CrewAI Tools and MCP
CrewAI Tools supports the Model Context Protocol (MCP). It gives you access to thousands of tools from the hundreds of MCP servers out there built by the community.
Before you start using MCP with CrewAI tools, you need to install the `mcp` extra dependencies:
```bash
pip install crewai-tools[mcp]
# or
uv add crewai-tools --extra mcp
```
To quickly get started with MCP in CrewAI you have 2 options:
### Option 1: Fully managed connection
In this scenario we use a contextmanager (`with` statement) to start and stop the the connection with the MCP server.
This is done in the background and you only get to interact with the CrewAI tools corresponding to the MCP server's tools.
For an STDIO based MCP server:
```python
from mcp import StdioServerParameters
from crewai_tools import MCPServerAdapter
serverparams = StdioServerParameters(
command="uvx",
args=["--quiet", "pubmedmcp@0.1.3"],
env={"UV_PYTHON": "3.12", **os.environ},
)
with MCPServerAdapter(serverparams) as tools:
# tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools
agent = Agent(..., tools=tools)
task = Task(...)
crew = Crew(..., agents=[agent], tasks=[task])
crew.kickoff(...)
```
For an SSE based MCP server:
```python
serverparams = {"url": "http://localhost:8000/sse"}
with MCPServerAdapter(serverparams) as tools:
# tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools
agent = Agent(..., tools=tools)
task = Task(...)
crew = Crew(..., agents=[agent], tasks=[task])
crew.kickoff(...)
```
### Option 2: More control over the MCP connection
If you need more control over the MCP connection, you can instanciate the MCPServerAdapter into an `mcp_server_adapter` object which can be used to manage the connection with the MCP server and access the available tools.
**important**: in this case you need to call `mcp_server_adapter.stop()` to make sure the connection is correctly stopped. We recommend that you use a `try ... finally` block run to make sure the `.stop()` is called even in case of errors.
Here is the same example for an STDIO MCP Server:
```python
from mcp import StdioServerParameters
from crewai_tools import MCPServerAdapter
serverparams = StdioServerParameters(
command="uvx",
args=["--quiet", "pubmedmcp@0.1.3"],
env={"UV_PYTHON": "3.12", **os.environ},
)
try:
mcp_server_adapter = MCPServerAdapter(serverparams)
tools = mcp_server_adapter.tools
# tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools
agent = Agent(..., tools=tools)
task = Task(...)
crew = Crew(..., agents=[agent], tasks=[task])
crew.kickoff(...)
# ** important ** don't forget to stop the connection
finally:
mcp_server_adapter.stop()
```
And finally the same thing but for an SSE MCP Server:
```python
from mcp import StdioServerParameters
from crewai_tools import MCPServerAdapter
serverparams = {"url": "http://localhost:8000/sse"}
try:
mcp_server_adapter = MCPServerAdapter(serverparams)
tools = mcp_server_adapter.tools
# tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools
agent = Agent(..., tools=tools)
task = Task(...)
crew = Crew(..., agents=[agent], tasks=[task])
crew.kickoff(...)
# ** important ** don't forget to stop the connection
finally:
mcp_server_adapter.stop()
```
### Considerations & Limitations
#### Staying Safe with MCP
Always make sure that you trust the MCP Server before using it. Using an STDIO server will execute code on your machine. Using SSE is still not a silver bullet with many injection possible into your application from a malicious MCP server.
#### Limitations
* At this time we only support tools from MCP Server not other type of primitives like prompts, resources...
* We only return the first text output returned by the MCP Server tool using `.content[0].text`
---
## Why Use CrewAI Tools?
- **Simplicity & Flexibility**: Easy-to-use yet powerful enough for complex workflows.

View File

@@ -66,3 +66,7 @@ from .aws import (
BedrockKBRetrieverTool,
BedrockInvokeAgentTool,
)
from .adapters.mcp_adapter import (
MCPServerAdapter,
)

View File

@@ -0,0 +1,142 @@
from __future__ import annotations
import logging
from typing import Any, TYPE_CHECKING
from crewai.tools import BaseTool
"""
MCPServer for CrewAI.
"""
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from mcp import StdioServerParameters
from mcpadapt.core import MCPAdapt
from mcpadapt.crewai_adapter import CrewAIAdapter
try:
from mcp import StdioServerParameters
from mcpadapt.core import MCPAdapt
from mcpadapt.crewai_adapter import CrewAIAdapter
MCP_AVAILABLE = True
except ImportError:
MCP_AVAILABLE = False
class MCPServerAdapter:
"""Manages the lifecycle of an MCP server and make its tools available to CrewAI.
Note: tools can only be accessed after the server has been started with the
`start()` method.
Attributes:
tools: The CrewAI tools available from the MCP server.
Usage:
# context manager + stdio
with MCPServerAdapter(...) as tools:
# tools is now available
# context manager + sse
with MCPServerAdapter({"url": "http://localhost:8000/sse"}) as tools:
# tools is now available
# manually stop mcp server
try:
mcp_server = MCPServerAdapter(...)
tools = mcp_server.tools
...
finally:
mcp_server.stop()
# Best practice is ensure cleanup is done after use.
mcp_server.stop() # run after crew().kickoff()
"""
def __init__(
self,
serverparams: StdioServerParameters | dict[str, Any],
):
"""Initialize the MCP Server
Args:
serverparams: The parameters for the MCP server it supports either a
`StdioServerParameters` or a `dict` respectively for STDIO and SSE.
"""
super().__init__()
self._adapter = None
self._tools = None
if not MCP_AVAILABLE:
import click
if click.confirm(
"You are missing the 'mcp' package. Would you like to install it?"
):
import subprocess
try:
subprocess.run(["uv", "add", "mcp crewai-tools[mcp]"], check=True)
except subprocess.CalledProcessError:
raise ImportError("Failed to install mcp package")
else:
raise ImportError(
"`mcp` package not found, please run `uv add crewai-tools[mcp]`"
)
try:
self._serverparams = serverparams
self._adapter = MCPAdapt(self._serverparams, CrewAIAdapter())
self.start()
except Exception as e:
if self._adapter is not None:
try:
self.stop()
except Exception as stop_e:
logger.error(f"Error during stop cleanup: {stop_e}")
raise RuntimeError(f"Failed to initialize MCP Adapter: {e}") from e
def start(self):
"""Start the MCP server and initialize the tools."""
self._tools = self._adapter.__enter__()
def stop(self):
"""Stop the MCP server"""
self._adapter.__exit__(None, None, None)
@property
def tools(self) -> list[BaseTool]:
"""The CrewAI tools available from the MCP server.
Raises:
ValueError: If the MCP server is not started.
Returns:
The CrewAI tools available from the MCP server.
"""
if self._tools is None:
raise ValueError(
"MCP server not started, run `mcp_server.start()` first before accessing `tools`"
)
return self._tools
def __enter__(self):
"""
Enter the context manager. Note that `__init__()` already starts the MCP server.
So tools should already be available.
"""
return self.tools
def __exit__(self, exc_type, exc_value, traceback):
"""Exit the context manager."""
return self._adapter.__exit__(exc_type, exc_value, traceback)

View File

@@ -0,0 +1,104 @@
from textwrap import dedent
import pytest
from mcp import StdioServerParameters
from crewai_tools import MCPServerAdapter
@pytest.fixture
def echo_server_script():
return dedent(
'''
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo Server")
@mcp.tool()
def echo_tool(text: str) -> str:
"""Echo the input text"""
return f"Echo: {text}"
mcp.run()
'''
)
@pytest.fixture
def echo_server_sse_script():
return dedent(
'''
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000)
@mcp.tool()
def echo_tool(text: str) -> str:
"""Echo the input text"""
return f"Echo: {text}"
mcp.run("sse")
'''
)
@pytest.fixture
def echo_sse_server(echo_server_sse_script):
import subprocess
import time
# Start the SSE server process with its own process group
process = subprocess.Popen(
["python", "-c", echo_server_sse_script],
)
# Give the server a moment to start up
time.sleep(1)
try:
yield {"url": "http://127.0.0.1:8000/sse"}
finally:
# Clean up the process when test is done
process.kill()
process.wait()
def test_context_manager_syntax(echo_server_script):
serverparams = StdioServerParameters(
command="uv", args=["run", "python", "-c", echo_server_script]
)
with MCPServerAdapter(serverparams) as tools:
assert len(tools) == 1
assert tools[0].name == "echo_tool"
assert tools[0].run(text="hello") == "Echo: hello"
def test_context_manager_syntax_sse(echo_sse_server):
sse_serverparams = echo_sse_server
with MCPServerAdapter(sse_serverparams) as tools:
assert len(tools) == 1
assert tools[0].name == "echo_tool"
assert tools[0].run(text="hello") == "Echo: hello"
def test_try_finally_syntax(echo_server_script):
serverparams = StdioServerParameters(
command="uv", args=["run", "python", "-c", echo_server_script]
)
try:
mcp_server_adapter = MCPServerAdapter(serverparams)
tools = mcp_server_adapter.tools
assert len(tools) == 1
assert tools[0].name == "echo_tool"
assert tools[0].run(text="hello") == "Echo: hello"
finally:
mcp_server_adapter.stop()
def test_try_finally_syntax_sse(echo_sse_server):
sse_serverparams = echo_sse_server
mcp_server_adapter = MCPServerAdapter(sse_serverparams)
try:
tools = mcp_server_adapter.tools
assert len(tools) == 1
assert tools[0].name == "echo_tool"
assert tools[0].run(text="hello") == "Echo: hello"
finally:
mcp_server_adapter.stop()