Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/praisonai-agents/praisonaiagents/mcp/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@
from typing import Any, List, Optional, Callable, Iterable, Union
from functools import wraps, partial

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
try:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
MCP_AVAILABLE = True
except ImportError:
MCP_AVAILABLE = False
ClientSession = None
StdioServerParameters = None
stdio_client = None

class MCPToolRunner(threading.Thread):
"""A dedicated thread for running MCP operations."""
Expand Down Expand Up @@ -196,10 +203,17 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6
debug: Enable debug logging for MCP operations (default: False)
**kwargs: Additional parameters for StdioServerParameters
"""
# Check if MCP is available
if not MCP_AVAILABLE:
raise ImportError(
"MCP (Model Context Protocol) package is not installed. "
"Install it with: pip install praisonaiagents[mcp]"
)

# Handle backward compatibility with named parameter 'command'
if command_or_string is None and command is not None:
command_or_string = command

# Set up logging - default to WARNING level to hide INFO messages
if debug:
logging.getLogger("mcp-wrapper").setLevel(logging.DEBUG)
Expand Down
26 changes: 23 additions & 3 deletions src/praisonai-agents/praisonaiagents/mcp/mcp_http_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@
from typing import List, Dict, Any, Optional, Callable, Iterable, Union
from urllib.parse import urlparse, urljoin

from mcp import ClientSession
try:
from mcp import ClientSession
MCP_AVAILABLE = True
except ImportError:
MCP_AVAILABLE = False
ClientSession = None

try:
import aiohttp
except ImportError:
raise ImportError("aiohttp is required for HTTP Stream transport. Install with: pip install praisonaiagents[mcp]")
aiohttp = None

logger = logging.getLogger("mcp-http-stream")

Expand Down Expand Up @@ -507,9 +513,23 @@ def __init__(self, server_url: str, debug: bool = False, timeout: int = 60, opti
timeout: Timeout in seconds for operations (default: 60)
options: Additional configuration options for the transport
"""
# Check if MCP is available
if not MCP_AVAILABLE:
raise ImportError(
"MCP (Model Context Protocol) package is not installed. "
"Install it with: pip install praisonaiagents[mcp]"
)

# Check if aiohttp is available
if aiohttp is None:
raise ImportError(
"aiohttp is required for HTTP Stream transport. "
"Install it with: pip install praisonaiagents[mcp]"
)

# Parse URL to extract base URL and endpoint
parsed = urlparse(server_url)

# If the URL already has a path, use it; otherwise use default /mcp endpoint
if parsed.path and parsed.path != '/':
self.base_url = server_url
Expand Down
19 changes: 16 additions & 3 deletions src/praisonai-agents/praisonaiagents/mcp/mcp_sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
import json
from typing import List, Dict, Any, Optional, Callable, Iterable

from mcp import ClientSession
from mcp.client.sse import sse_client
try:
from mcp import ClientSession
from mcp.client.sse import sse_client
MCP_AVAILABLE = True
except ImportError:
MCP_AVAILABLE = False
ClientSession = None
sse_client = None

logger = logging.getLogger("mcp-sse")

Expand Down Expand Up @@ -169,12 +175,19 @@ def __init__(self, server_url: str, debug: bool = False, timeout: int = 60):
debug: Whether to enable debug logging
timeout: Timeout in seconds for operations (default: 60)
"""
# Check if MCP is available
if not MCP_AVAILABLE:
raise ImportError(
"MCP (Model Context Protocol) package is not installed. "
"Install it with: pip install praisonaiagents[mcp]"
)

self.server_url = server_url
self.debug = debug
self.timeout = timeout
self.session = None
self.tools = []

# Set up logging
if debug:
logger.setLevel(logging.DEBUG)
Expand Down
71 changes: 71 additions & 0 deletions src/praisonai-agents/tests/test_mcp_optional_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Test that MCP module imports are optional and don't break basic functionality.
This test verifies the fix for issue #1091.
"""
import sys
import pytest
Comment on lines +5 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To properly test for missing optional dependencies by mocking imports, we need to use patch from unittest.mock. Please add this import.

Suggested change
import sys
import pytest
import sys
import pytest
from unittest.mock import patch



class TestMCPOptionalImport:
"""Test that MCP is properly optional."""

def test_praisonaiagents_import_without_mcp(self):
"""Test that praisonaiagents can be imported without mcp installed."""
# This should not raise any ImportError
import praisonaiagents
assert praisonaiagents is not None

def test_agent_import_without_mcp(self):
"""Test that Agent can be imported without mcp installed."""
from praisonaiagents import Agent
assert Agent is not None

def test_mcp_lazy_loading(self):
"""Test that MCP is lazily loaded."""
from praisonaiagents import MCP
# MCP should either be available or None (not raise ImportError on access)
# The actual ImportError should only be raised when trying to instantiate
# if mcp package is not installed

Comment on lines +23 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The test_mcp_lazy_loading test is currently a placeholder without any assertions. It's crucial to verify that instantiating the MCP-related classes correctly raises an ImportError when their dependencies are missing. This suggestion replaces the placeholder with comprehensive tests for this behavior, covering MCP, SSEMCPClient, and HTTPStreamMCPClient.

    def test_mcp_instantiation_raises_error_if_mcp_unavailable(self):
        """Test that instantiating MCP classes raises ImportError if mcp is not installed."""
        from unittest.mock import patch
        with patch.dict('sys.modules', {'mcp': None, 'mcp.client.stdio': None, 'mcp.client.sse': None}):
            import importlib

            # Test MCP class
            from praisonaiagents.mcp import mcp
            importlib.reload(mcp)
            assert not mcp.MCP_AVAILABLE
            with pytest.raises(ImportError, match="MCP .* not installed"):
                mcp.MCP("some command")
            importlib.reload(mcp)  # Restore

            # Test SSEMCPClient class
            from praisonaiagents.mcp import mcp_sse
            importlib.reload(mcp_sse)
            assert not mcp_sse.MCP_AVAILABLE
            with pytest.raises(ImportError, match="MCP .* not installed"):
                mcp_sse.SSEMCPClient("http://localhost/sse")
            importlib.reload(mcp_sse)  # Restore

            # Test HTTPStreamMCPClient class for MCP
            from praisonaiagents.mcp import mcp_http_stream
            importlib.reload(mcp_http_stream)
            assert not mcp_http_stream.MCP_AVAILABLE
            with pytest.raises(ImportError, match="MCP .* not installed"):
                mcp_http_stream.HTTPStreamMCPClient("http://localhost/stream")
            importlib.reload(mcp_http_stream)  # Restore

    def test_http_stream_instantiation_raises_error_if_aiohttp_unavailable(self):
        """Test that instantiating HTTPStreamMCPClient raises ImportError if aiohttp is not installed."""
        from unittest.mock import patch
        with patch.dict('sys.modules', {'aiohttp': None}):
            import importlib
            from praisonaiagents.mcp import mcp_http_stream
            importlib.reload(mcp_http_stream)
            
            with pytest.raises(ImportError, match="aiohttp is required"):
                mcp_http_stream.HTTPStreamMCPClient("http://localhost/stream")
            
            importlib.reload(mcp_http_stream)  # Restore

def test_mcp_module_import_no_crash(self):
"""Test that mcp module can be imported without crashing."""
# This tests that the try/except in mcp.py works correctly
try:
from praisonaiagents.mcp import mcp as mcp_module
assert hasattr(mcp_module, 'MCP_AVAILABLE')
except ImportError:
# If import fails, it should be because of missing mcp,
# not because of a syntax error
pass

def test_mcp_sse_module_import_no_crash(self):
"""Test that mcp_sse module can be imported without crashing."""
try:
from praisonaiagents.mcp import mcp_sse
assert hasattr(mcp_sse, 'MCP_AVAILABLE')
except ImportError:
pass

def test_mcp_http_stream_module_import_no_crash(self):
"""Test that mcp_http_stream module can be imported without crashing."""
try:
from praisonaiagents.mcp import mcp_http_stream
assert hasattr(mcp_http_stream, 'MCP_AVAILABLE')
except ImportError:
pass

def test_basic_agent_creation_without_mcp_tools(self):
"""Test that agents can be created without MCP tools."""
from praisonaiagents import Agent

# Creating an agent without MCP tools should work
agent = Agent(
name="Test Agent",
instructions="You are a test agent.",
llm="gpt-4o-mini"
)
assert agent is not None


if __name__ == "__main__":
pytest.main([__file__, "-v"])
Loading