Skip to content
Open
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
8 changes: 8 additions & 0 deletions aisuite/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
except ImportError:
MCP_AVAILABLE = False

# Define a fallback is_mcp_config when MCP is not installed
# This allows checking for MCP configs to provide helpful error messages
def is_mcp_config(obj: Any) -> bool:
"""Check if an object looks like an MCP config dict."""
if not isinstance(obj, dict):
return False
return obj.get("type") == "mcp" and "name" in obj


class Client:
def __init__(
Expand Down
107 changes: 107 additions & 0 deletions tests/client/test_mcp_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Tests for MCP fallback behavior when MCP package is not installed.

This tests the fix for the NameError that occurred when using tools
without the MCP package installed.
"""

import pytest
from unittest.mock import Mock, patch


class TestMCPFallback:
"""Test MCP fallback behavior when MCP is not available."""

def test_is_mcp_config_fallback_exists(self):
"""
Test that is_mcp_config is always defined, even without MCP.

This is a regression test for the bug where using tools without
MCP installed would raise: NameError: name 'is_mcp_config' is not defined
"""
# Import the client module - is_mcp_config should always be defined
from aisuite import client as client_module

# Verify is_mcp_config exists (either from MCP or fallback)
assert hasattr(client_module, 'is_mcp_config') or client_module.MCP_AVAILABLE

def test_mcp_config_detection(self):
"""Test that MCP configs are correctly detected."""
# Import after any patching
from aisuite.client import MCP_AVAILABLE

# We need to test the fallback function directly
# Define the same logic as the fallback
def is_mcp_config_fallback(obj):
if not isinstance(obj, dict):
return False
return obj.get("type") == "mcp" and "name" in obj

# Test MCP config detection
mcp_config = {"type": "mcp", "name": "test", "command": "npx"}
assert is_mcp_config_fallback(mcp_config) == True

# Test non-MCP configs
function_tool = {"type": "function", "function": {"name": "test"}}
assert is_mcp_config_fallback(function_tool) == False

# Test non-dict inputs
assert is_mcp_config_fallback("not a dict") == False
assert is_mcp_config_fallback(None) == False


class TestIsMCPConfigFallback:
"""Direct tests for the is_mcp_config fallback function logic."""

def test_valid_mcp_config(self):
"""Test detection of valid MCP configs."""
# Inline the fallback logic for testing
def is_mcp_config(obj):
if not isinstance(obj, dict):
return False
return obj.get("type") == "mcp" and "name" in obj

valid_configs = [
{"type": "mcp", "name": "filesystem"},
{"type": "mcp", "name": "test", "command": "npx", "args": ["arg1"]},
{"type": "mcp", "name": "server", "extra": "data"},
]

for config in valid_configs:
assert is_mcp_config(config) == True, f"Should detect {config} as MCP config"

def test_invalid_mcp_config(self):
"""Test that non-MCP configs are not detected as MCP."""
def is_mcp_config(obj):
if not isinstance(obj, dict):
return False
return obj.get("type") == "mcp" and "name" in obj

invalid_configs = [
{"type": "function", "function": {}}, # OpenAI function format
{"name": "test"}, # Missing type
{"type": "mcp"}, # Missing name
{"type": "other", "name": "test"}, # Wrong type
{}, # Empty dict
]

for config in invalid_configs:
assert is_mcp_config(config) == False, f"Should not detect {config} as MCP config"

def test_non_dict_inputs(self):
"""Test that non-dict inputs return False."""
def is_mcp_config(obj):
if not isinstance(obj, dict):
return False
return obj.get("type") == "mcp" and "name" in obj

non_dicts = [
"string",
123,
12.34,
None,
[],
]

for item in non_dicts:
assert is_mcp_config(item) == False, f"Should return False for {type(item)}"